I implemented ViewModel driven navigation as shown in my code below. Basic idea is a Singleton class NavigationManager which is available to both, composables and the ViewModel, via dependency injection. The NavigationManager has a SharedFlow property named direction which can be changed from e.g. the ViewModel and is observed by the composables.
Now my question on this:
Is it safe to use a SharedFlow in this situation? As a SharedFlow is a hot flow and therefore can emit events while not being observed, is it possible that navigation events are lost? E.g. is it possible that a navigation event is emitted while the user rotates his phone and the NavigationManger.direction SharedFlow isn't observed for a short time (as the activity is recreated on rotation)?
// MainActivity.kt
// navigationManager.direction is observed here
// NavigationManager is a Singleton injected via dependency injection
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
#Inject
lateinit var navigationManager: NavigationManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyJetpackComposeTheme {
val navController = rememberNavController()
MyNavHost(navController)
LaunchedEffect(navigationManager.direction) {
navigationManager.direction.collect { direction ->
direction?.let {
Log.i("NavTest", "change route to: $direction")
navController.navigate(direction)
}
}
}
}
}
}
}
// The navigation manager. Instantiating it is done by the
// depdendency injection framework, not shown here for brevitiy
class NavigationManager(private val externalScope: CoroutineScope) {
private val _direction = MutableSharedFlow<String?>()
val direction : SharedFlow<String?> = _direction
fun navigate(direction: String) {
Log.d("NavTest", "navigating to $direction")
externalScope.launch {
_direction.emit(direction)
}
}
}
// triggering navigation from inside a ViewModel would be like this
// (navigationManger would be injected via dependency injection)
navigationManager.navigate("some_direction")
Related
I'm subscribed to an observable in my Fragment, the observable listens for some user input from three different sources.
The main issue is that once I navigate to another Fragment and return to the one with the subscription, the data is duplicated as the observable is handled twice.
What is the correct way to handle a situation like this?
I've migrated my application to a Single-Activity and before it, the subscription was made in the activity without any problem.
Here is my Fragment code:
#AndroidEntryPoint
class ProductsFragment : Fragment() {
#Inject
lateinit var sharedPreferences: SharedPreferences
private var _binding: FragmentProductsBinding? = null
private val binding get() = _binding!!
private val viewModel: ProductsViewModel by viewModels()
private val scanner: CodeReaderViewModel by activityViewModels()
private fun observeBarcode() {
scanner.barcode.observe(viewLifecycleOwner) { barcode ->
if (barcode.isNotEmpty()) {
if (binding.searchView.isIconified) {
addProduct(barcode) // here if the fragment is resumed from a backstack the data is duplicated.
}
if (!binding.searchView.isIconified) {
binding.searchView.setQuery(barcode, true)
}
}
}
}
private fun addProduct(barcode: String) {
if (barcode.isEmpty()) {
return
}
viewModel.insert(barcode)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.start(args.documentId)
if (args.documentType == "Etichette") {
binding.cvLabels.visibility = View.VISIBLE
}
initUI()
observe()
}
private fun observe() {
observeBarcode()
observeProducts()
observeLoading()
observeLast()
}
}
Unfortunately, LiveData is a terribly bad idea (the way it was designed), Google insisted till they kinda phased it out (but not really since it's still there) that "it's just a value holder"...
Anyway... not to rant too much, the solution you have to use can be:
Use The "SingleLiveEvent" (method is officially "deprecated now" but... you can read more about it here).
Follow the "official guidelines" and use a Flow instead, as described in the official guideline for handling UI Events.
Update: Using StateFlow
The way to collect the flow is, for e.g. in a Fragment:
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { // or RESUMED
viewModel.yourFlow.collectLatest { ... } // or collect { ... }
}
}
For that in your ViewModel you'd expose something like:
Warning: Pseudo-Code
// Imagine your state is represented in this sealed class
sealed class State {
object Idle: State
object Loading: State
data class Success(val name: String): State
data class Failure(val reason: String): State
}
// You need an initial state
private val _yourFlow = MutableStateFlow(State.Idle)
val yourFlow: StateFlow<State> = _yourFlow
Then you can emit using
_yourFlow.emit(State.Loading)
Every time you call
scanner.barcode.observe(viewLifecycleOwner){
}
You are creating a new anonymous observer. So every new call to observe will add another observer that will get onChanged callbacks. You could move this observer out to be a property. With this solution observe won't register new observers.
Try
class property
val observer = Observer<String> { onChanged() }
inside your method
scanner.barcode.observe(viewLifecycleOwner, observer)
Alternatively you could keep your observe code as is but move it to a Fragment's callback that only gets called once fex. onCreate(). onCreate gets called only once per fragment instance whereas onViewCreated gets called every time the fragment's view is created.
Scenario
I have a hot flow EventHandler.sharedFlow emitted on a button click.
The flow is received by Repository that performs some action in OnEach{}.
The repository flow is then received by two event collectors EventCollectorA and EventCollectorB.
The event collector flows are then combined and collected in MyViewModel.
Issue
The two event collectors cause onEach{...} to run twice on every click. However I only want to run onEach{...} once and have it received in two event collectors. How can I achieve this?
Note: I am using Hilt to ony have one instance of Repository, EventCollectorA and EventCollectorB
Code
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
binding.buttonB.setOnClickListener {
viewModel.userClickEvent("Click Event")
}
}
}
#HiltViewModel
class MyViewModel #Inject constructor(
private val eventHandler: EventHandler,
private val eventCollectorA: EventCollectorA,
private val eventCollectorB: EventCollectorB,
) : ViewModel() {
fun userClickEvent(event: String) = viewModelScope.launch {
eventHandler.userClick(event)
}
init {
viewModelScope.launch {
combine(
eventCollectorA.sharedFlow,
eventCollectorB.sharedFlow
) { a, b ->
{/*do something*/}
}.collect()
}
}
}
class EventHandler {
private val _sharedFlow = MutableSharedFlow<String>()
val sharedFlow = _sharedFlow.asSharedFlow()
suspend fun userClick(event: String) {
_sharedFlow.emit(event)
}
}
class Repository constructor(
eventHandler: EventHandler,
) {
val sharedFlow = eventHandler.sharedFlow
.filter { it == "Click Event" }
.onEach {/*do something*/} /*onEach is called twice on click event. I only want it called once*/
.onStart { emit("Begin") }
}
class EventCollectorA constructor(repository: Repository) {
val sharedFlow = repository.sharedFlow.map {
it
}
}
class EventCollectorB constructor(repository: Repository) {
val sharedFlow = repository.sharedFlow.map {
it
}
}
The problem here is that while eventHandler.sharedFlow is a SharedFlow, after applying any operators to it, we get a regular, not shared flow. filter(), onEach() and onStart() are running separately for each new collection. If you want to share them between collections, you need to construct another shared flow, after applying them:
val sharedFlow = eventHandler.sharedFlow
.filter { it == "Click Event" }
.onEach {/*do something*/}
.onStart { emit("Begin") }
.shareIn(...)
Further explanation
We need to be aware that a regular, cold flow is not like a live stream of data. It is more like a source of such streams and with each new collection we start entirely new stream of data. For example, if we create a flow using flow { } builder, we have only a single flow object, but if we invoke collect {} multiple times on it, then for each collection the lambda will be invoked again and again. Similarly, each operator that we use to construct a new flow, is also invoked separately for each collection.
You can think of shareIn() as creating a service that observes its upstream flow and duplicates its data to each of its downstream flows. No matter how many times we collect the shared flow, upstream flow will be collected only once. Operators above shareIn() will be invoked once, while operators below shareIn() will be invoked separately for each collection.
I need to open a Compose component with its own ViewModel and pass arguments to it, but at the same time I inject dependencies to this ViewModel. How can I achieve this? Can I combine ViewModel factory and Dependency Injection (Hilt)?
Yes. you can..
Have your component be like this:
#Composable
fun MyScreen(
viewModel: MyViewModel = hiltViewModel()
) {
...
}
and in your viewModel:
#HiltViewModel
class MyViewModel #Inject constructor(
private val repository: MyRepository,
... //If you have any other dependencies, add them here
): ViewModel() {
...
}
When you pass arguments to the ViewModel, make sure that Hilt knows where to get that dependency. If you follow the MVVM architecture, then the ViewModel should handle all the data and the composable all the ui related components. So usually, you only need the ViewModel injection into the composable and all the other data injected dependencies into the ViewModel.
The composable should only care about the data that it gets from the ViewModel. Where the ViewModel gets that data and the operations it does on that data, it does not care.
Lemme know if this is what you meant..
Check out the official website for more:
Hilt-Android
Yes, you can. This is called "Assisted Inject" and it has it's own solutions in Hilt, Dagger(since version 2.31) and other libraries like AutoFactory or square/AssistedInject.
In this article, you can find an example of providing AssistedInject in ViewModel for Composable with Hilt Entry points.
Here is some code from article in case if article would be deleted:
In the main Activity, we’ll need to declare EntryPoint interface which will provide Factory for creating ViewModel:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
#EntryPoint
#InstallIn(ActivityComponent::class)
interface ViewModelFactoryProvider {
fun noteDetailViewModelFactory(): NoteDetailViewModel.Factory
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NotyTheme {
NotyNavigation()
}
}
}
}
We get Factory from Activity and instantiating our ViewModel with that Factory and assisted some field:
#Composable
fun noteDetailViewModel(noteId: String): NoteDetailViewModel {
val factory = EntryPointAccessors.fromActivity(
LocalContext.current as Activity,
MainActivity.ViewModelFactoryProvider::class.java
).noteDetailViewModelFactory()
return viewModel(factory = NoteDetailViewModel.provideFactory(factory, noteId))
}
Now just go to your navigation components and use this method to provide ViewModel to your Composable screen as following:
NavHost(navController, startDestination = Screen.Notes.route, route = NOTY_NAV_HOST_ROUTE) {
composable(
Screen.NotesDetail.route,
arguments = listOf(navArgument(Screen.NotesDetail.ARG_NOTE_ID) { type = NavType.StringType })
) {
val noteId = it.arguments?.getString(Screen.NotesDetail.ARG_NOTE_ID)!!
NoteDetailsScreen(navController, noteDetailViewModel(noteId))
}
}
I am making an app where the user first need to login to be able to get alot of different data from a backend. (many endpoints)
So I have one viewmodel for the login, and I have alot of viewmodels for all the other data.
The other viewmodels require the token from the first viewmodel to be able to get data from the backend.
I don't know how I can do this.
I was thinking that I can have my login screen in a kind of state manager which will direct the UI to the correct screen like this
#ExperimentalComposeUiApi
#Composable
fun LoginState(vm: AuthViewModel, nc: NavController) {
val token by vm.token.collectAsState()
when (token) {
is Resource.Loading -> {
LoadingScreen()
}
is Resource.Success -> {
Scaffold(vm = vm)
}
is Resource.Error -> {
LoginScreen(vm = vm)
}
}
}
But then I would have to create the viewmodels inside the Scaffold which is a composable function, and that is not possible.
Another thought was to use Hilt to do some kind of magic dependency injection, and then put all the viewmodels into a ViewModelManager in the MainActivity and then inject the Token into the repositories of each viewmodel when login is successfull.
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val authViewModel: AuthViewModel by viewModels()
private val userViewModel: UserViewModel by viewModels()
private val venueViewModel: VenueViewModel by viewModels()
private val eventViewModel: EventViewModel by viewModels()
private val viewModelManager = ViewModelManager(
userViewModel = userViewModel,
authViewModel = authViewModel,
venueViewModel = venueViewModel,
eventViewModel = eventViewModel,
)
#ExperimentalMaterialApi
#ExperimentalComposeUiApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MoroAdminTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
ScaffoldExample(viewModelManager)
}
}
}
}
}
However I have no idea how to do this or if it is even possible - or a good solution.
Problem: you want to share a value (token) to all of your view model
your token retrieved in AuthViewModel and need to share it to the other viewModels
you can make your data in the other viewModels changes when the token changes
by using datastore Preferences see implementation
Datastore preferences provides you with a flow of values whenever the value changes
Create a DatastoreManager Class
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
#Singleton
class DatastoreManager #Inject constructor(#ApplicationContext context: Context) {
private val dataStore = context.dataStore
val dataFlow = dataStore.data
.map { preferences ->
val token = preferences[PreferencesKeys.TOKEN]
}
suspend fun updateToken(token: String) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.TOKEN] = token
}
}
private object PreferencesKeys {
val TOKEN = preferencesKey<String>("token")
}
}
In AuthViewModel
Inject the DatastoreManager and set the token after login
datastore.updateToken(newToken)
In other ViewModels
Inject the DatastoreManager and use it's value
//this is a flow of tokens and will recive the token when you set it
val token = datastore.token
// if you are not familiar with flows and using only LiveData
val token = datastore.token.asLiveData()
// use the token to get the data from backend
val data = token.map {
// this code will trigger every time the token changes
yourGetDataFromBackendFunction(it)
}
But then I would have to create the viewmodels inside the Scaffold which is a composable function, and that is not possible.
This is not true. You don't have to create view models in your Activity.
In any composable you can use viewModel()
Returns an existing ViewModel or creates a new one in the given owner (usually, a fragment or an activity), defaulting to the owner provided by LocalViewModelStoreOwner.
So you don't need any ViewModelManager. Inside any composable you can use viewModel() with the corresponding class. In your case you're using Hilt, you should use hiltViewModel() instead: it'll also initialize your injections.
#Composable
fun AuthScreen(viewModel: AuthViewModel = hiltViewModel()) {
}
Or like this:
#Composable
fun VenueScreen() {
val viewModel: VenueViewModel = hiltViewModel()
}
First approach will allow you to easily test your screen with mock view model, without passing any arguments in your production code.
Check out more about view models in view models documentation and hilt documentation
As to your token question, you can pass it with injections. I don't think that your view model really needs the token, probably you should have some network manager which will use the token to make requests. And this network manager should use injection of some token provider.
I am trying to add Dagger 2 to my project. I was able to inject ViewModels (AndroidX Architecture component) for my fragments.
I have a ViewPager which has 2 instances of the same fragment (Only a minor change for each tabs) and in each tab, I am observing a LiveData to get updated on data change (from API).
The issue is that when the api response comes and updates the LiveData, the same data in the currently visible fragment is being sent to observers in all the tabs. (I think this is probably because of the scope of the ViewModel).
This is how I am observing my data:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
activityViewModel.expenseList.observe(this, Observer {
swipeToRefreshLayout.isRefreshing = false
viewAdapter.setData(it)
})
....
}
I am using this class for providing ViewModels:
class ViewModelProviderFactory #Inject constructor(creators: MutableMap<Class<out ViewModel?>?, Provider<ViewModel?>?>?) :
ViewModelProvider.Factory {
private val creators: MutableMap<Class<out ViewModel?>?, Provider<ViewModel?>?>? = creators
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
var creator: Provider<out ViewModel?>? = creators!![modelClass]
if (creator == null) { // if the viewmodel has not been created
// loop through the allowable keys (aka allowed classes with the #ViewModelKey)
for (entry in creators.entries) { // if it's allowed, set the Provider<ViewModel>
if (modelClass.isAssignableFrom(entry.key!!)) {
creator = entry.value
break
}
}
}
// if this is not one of the allowed keys, throw exception
requireNotNull(creator) { "unknown model class $modelClass" }
// return the Provider
return try {
creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
companion object {
private val TAG: String? = "ViewModelProviderFactor"
}
}
I am binding my ViewModel like this:
#Module
abstract class ActivityViewModelModule {
#MainScope
#Binds
#IntoMap
#ViewModelKey(ActivityViewModel::class)
abstract fun bindActivityViewModel(viewModel: ActivityViewModel): ViewModel
}
I am using #ContributesAndroidInjector for my fragment like this:
#Module
abstract class MainFragmentBuildersModule {
#ContributesAndroidInjector
abstract fun contributeActivityFragment(): ActivityFragment
}
And I am adding these modules to my MainActivity subcomponent like this:
#Module
abstract class ActivityBuilderModule {
...
#ContributesAndroidInjector(
modules = [MainViewModelModule::class, ActivityViewModelModule::class,
AuthModule::class, MainFragmentBuildersModule::class]
)
abstract fun contributeMainActivity(): MainActivity
}
Here is my AppComponent:
#Singleton
#Component(
modules =
[AndroidSupportInjectionModule::class,
ActivityBuilderModule::class,
ViewModelFactoryModule::class,
AppModule::class]
)
interface AppComponent : AndroidInjector<SpenmoApplication> {
#Component.Builder
interface Builder {
#BindsInstance
fun application(application: Application): Builder
fun build(): AppComponent
}
}
I am extending DaggerFragment and injecting ViewModelProviderFactory like this:
#Inject
lateinit var viewModelFactory: ViewModelProviderFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
....
activityViewModel =
ViewModelProviders.of(this, viewModelFactory).get(key, ActivityViewModel::class.java)
activityViewModel.restartFetch(hasReceipt)
}
the key will be different for both the fragments.
How can I make sure that only the observer of the current fragment is getting updated.
EDIT 1 ->
I have added a sample project with the error. Seems like the issue is happening only when a custom scope is added. Please check out the sample project here: Github link
master branch has the app with the issue. If you refresh any tab (swipe to refresh) the updated value is getting reflected in both the tabs. This is only happening when I add a custom scope to it (#MainScope).
working_fine branch has the same app with no custom scope and its working fine.
Please let me know if the question is not clear.
I want to recap the original question, here's it:
I am currently using the working fine_branch, but I want to know, why would using scope break this.
As per my understanding your have an impression, that just because you are trying to obtain an instance of ViewModel using different keys, then you should be provided different instances of ViewModel:
// in first fragment
ViewModelProvider(...).get("true", PagerItemViewModel::class.java)
// in second fragment
ViewModelProvider(...).get("false", PagerItemViewModel::class.java)
The reality, is a bit different. If you put following log in fragment you'll see that those two fragments are using the exact same instance of PagerItemViewModel:
Log.i("vvv", "${if (oneOrTwo) "one:" else "two:"} viewModel hash is ${viewModel.hashCode()}")
Let's dive in and understand why this happens.
Internally ViewModelProvider#get() will try to obtain an instance of PagerItemViewModel from a ViewModelStore which is basically a map of String to ViewModel.
When FirstFragment asks for an instance of PagerItemViewModel the map is empty, hence mFactory.create(modelClass) is executed, which ends up in ViewModelProviderFactory. creator.get() ends up calling DoubleCheck with following code:
public T get() {
Object result = instance;
if (result == UNINITIALIZED) { // 1
synchronized (this) {
result = instance;
if (result == UNINITIALIZED) {
result = provider.get();
instance = reentrantCheck(instance, result); // 2
/* Null out the reference to the provider. We are never going to need it again, so we
* can make it eligible for GC. */
provider = null;
}
}
}
return (T) result;
}
The instance is now null, hence a new instance of PagerItemViewModel is created and is saved in instance (see // 2).
Now the exact same procedure happens for SecondFragment:
fragment asks for an instance of PagerItemViewModel
map now is not empty, but does not contain an instance of PagerItemViewModel with key false
a new instance of PagerItemViewModel is initiated to be created via mFactory.create(modelClass)
Inside ViewModelProviderFactory execution reaches creator.get() whose implementation is DoubleCheck
Now, the key moment. This DoubleCheck is the same instance of DoubleCheck that was used for creating ViewModel instance when FirstFragment asked for it. Why is it the same instance? Because you've applied a scope to the provider method.
The if (result == UNINITIALIZED) (// 1) is evaluating to false and the exact same instance of ViewModel is being returned to the caller - SecondFragment.
Now, both fragments are using the same instance of ViewModel hence it is perfectly fine that they are displaying the same data.
Both the fragments receive the update from livedata because viewpager keeps both the fragments in resumed state.
Since you require the update only on the current fragment visible in the viewpager, the context of the current fragment is defined by the host activity, the activity should explicitly direct updates to the desired fragment.
You need to maintain a map of Fragment to LiveData containing entries for all the fragments(make sure to have an identifier that can differentiate two fragment instances of the same fragment) added to viewpager.
Now the activity will have a MediatorLiveData observing the original livedata observed by the fragments directly. Whenever the original livedata posts an update, it will be delivered to mediatorLivedata and the mediatorlivedata in turen will only post the value to livedata of the current selected fragment. This livedata will be retrieved from the map above.
Code impl would look like -
class Activity {
val mapOfFragmentToLiveData<FragmentId, MutableLiveData> = mutableMapOf<>()
val mediatorLiveData : MediatorLiveData<OriginalData> = object : MediatorLiveData() {
override fun onChanged(newData : OriginalData) {
// here get the livedata observed by the currently selected fragment
val currentSelectedFragmentLiveData = mapOfFragmentToLiveData.get(viewpager.getSelectedItem())
// now post the update on this livedata
currentSelectedFragmentLiveData.value = newData
}
}
fun getOriginalLiveData(fragment : YourFragment) : LiveData<OriginalData> {
return mapOfFragmentToLiveData.get(fragment) ?: MutableLiveData<OriginalData>().run {
mapOfFragmentToLiveData.put(fragment, this)
}
}
class YourFragment {
override fun onActivityCreated(bundle : Bundle){
//get activity and request a livedata
getActivity().getOriginalLiveData(this).observe(this, Observer { _newData ->
// observe here
})
}
}