In my Android app, I pass custom data (UByteArray) from one activity to another using the parcelable interface.
I am using this data inside multiple fragments, so I rewrote the data class to extend androidx ViewModel and expose LiveData properties to the fragments. Now the UI updates are a lot nicer, but I think I am using it wrong because I overwrite all ViewModel values inside onCreate.
Now my question: What do I need to change to initialize the ViewModel only once?
The following is my current code (abbreviated and renamed for this question):
class ActivityB : AppCompatActivity() {
private val bData: ViewModelB by viewModels()
// ...
override fun onCreate(savedInstanceState: Bundle?) {
// ...
intent.getParcelableExtra<ViewModelB>("id")?.let {
Log.e(TAG, "Found parceled bData $it")
// This seems like a very stupid way to do it, is there a better one?
bData.copyAll(it)
}
}
}
I saw that it is possible to inject SavedState into the ViewModelB constructor, but I don't have a saved state until now, and the data needs to be passed only once.
Should I change the initialization of tagData with by viewModels() to = ViewModelB(intent)?
Or do I need to extend the ViewModelFactory somehow?
Any tip here would be really appreciated, thanks.
I saw that it is possible to inject SavedState into the ViewModelB constructor, but I don't have a saved state until now, and the data needs to be passed only once.
The official solution would be to provide a SavedStateHandle that is initialized with the defaultArgs as the intent.extras of your Activity.
For that, you need to provide an AbstractSavedStateViewModelFactory implementation, OR use SavedStateViewModelFactory (in which case you must define the right constructor in order to have it instantiated via reflection).
class ActivityB : AppCompatActivity() {
private val bData: ViewModelB by viewModels {
SavedStateViewModelFactory(application, this, intent.extras)
}
// ...
override fun onCreate(savedInstanceState: Bundle?) {
// ...
// intent.getParcelableExtra<ViewModelB>("id")?.let {
// Log.e(TAG, "Found parceled bData $it")
}
}
Then in your ViewModel
#Keep
class ViewModelB(val savedStateHandle: SavedStateHandle): ViewModel() {
val uByteData = savedStateHandle.get<UByteArray>("id")
}
Or so. The "id" key must match the same key as is in the intent extras.
Since you have a ViewModel which implements Parcelable, you can get your ViewModelB instance directly from the Intent extra.
The Intent which is used for starting ActivityB may not be != null at the time when ActivityB is instantiated, but you can use
lateinit var bData: ViewModelB
Then in onCreate()
bData = if(intent.hasExtra("id")) intent.getParcelableExtra<ViewModelB>("id") else ViewModelProvider(this).get(ViewModelB::class.java)
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.
I am trying to understand how can I use my same viewmodel across different fragments which even belongs to different activities.
So let's say I have Activity1 with Fragment A, Fragment B and Activity2 with Fragment C. How do I create a single instance of viewmodel that I can use across all these fragments.
I tried understanding shared viewmodel but seems like it is to be used if sharing data between fragments of a single activity and not multiple activities.
So basically I want to create a single instance of viewmodel across all the fragments? How can I achieve this functionality also keeping in mind the MVVM approach.
Any help would be appreciated. Thanks!
This is not supported. Google's recommendation is to put all your screens in a single Activity.
But, you can make an intermediate singleton class that each instance of the ViewModel uses.
Or maybe you could use a factory that treats it like a temporary singleton and does reference counting so it doesn't get cleared too early or hang onto the reference for too long. Untested example of what I mean:
private var viewModelInstance: MyViewModel? = null
private var refCount = 0
class MyViewModel: ViewModel() {
override fun onCleared() {
if (--refCount > 0) {
return
}
viewModelInstance = null
// Do typical onCleared cleanup here
}
}
class MyViewModelFactory: ViewModelProvider.Factory {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
require(modelClass.isAssignableFrom(MyViewModel::class.java)) { "Factory only supports MyViewModel" }
refCount++
viewModelInstance = viewModelInstance ?: MyViewModel()
return viewModelInstance as T
}
}
Shared viewmodel between multiple activities is not supported.
One way to achieve this using AndroidViewModel. You can create a ViewModel extending AndroidViewModel . This requires application instance. This viewmodel will be binded to application lifecycle and same instance will be available through out the lifecycle of the application. In one activity you can add data, and in other activity you can get updated data.
This will be acting something like singleton instance(But not exactly).
Addition to this, you can also use live data in AndroidViewModel if you use observer with activity/fragment lifecycle owner. So the observer will be live only till life cycle of fragment or activity.
ViewModel:
class AppViewModel constructor(private val mApplication: Application) :
AndroidViewModel(mApplication) {
//ViewModel Logic
}
Initializing Viewmodel:
You can initialize viewmodel like this in any fragment or activity.
val appViewModel = ViewModelProvider.AndroidViewModelFactory.getInstance(MyApp.getInstance())
.create(AppViewModel::class.java)
Application Class:
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
mInstance = this
}
companion object {
lateinit var mInstance: CooperApp
#Synchronized
fun getInstance(): MyApp {
return mInstance
}
}
}
Also one more thing we can use is like initializing viewmodel in application class and create similar function to getInstance() which will return viewmodel instance and use it all over the app
I have this database that contains information about celebrities, lots of information, ie movies, roles, articles...
Without the viewmodel, all of the stuff in the edit text gets lost. So i dont want to lose all of that
info on rotation.
So i send an intent with all the relevant info of a selected celebrity(sql room) to the addeditactivity, so now how do i initialize the viewmodel? If I do getStringExtra in onCreate, wouldn't that just rewrite the viewmodel again when the activity gets recreated?
how do i get around this, also is there a better alternative? im a beginner, thanks in advance!
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// ......... shared preferences code .................
// get intent code ................
mViewModel = ViewModelProviders.of(this).get(testViewModel.class);
mViewModel.celebName = intent.getStringExtra(CELEB_NAME);
// similar..........
}
No, ViewModels have separate lifecyles from Activities. When your Activity gets rotated/recreated it will still use your previously-instantiated ViewModel (and any data saved in it) unless it has already been cleared.
See https://developer.android.com/topic/libraries/architecture/viewmodel
If the activity is re-created, it receives the same MyViewModel
instance that was created by the first activity. When the owner
activity is finished, the framework calls the ViewModel objects's
onCleared() method so that it can clean up resources.
You can initialize your viewModel in onCreate() using ViewModelFactory to pass your data from getStringExtra to your viewModel. Use viewModelFactory pattern to pass data to your viewModel. Here "YourINFOParameter" is your data from getStringExtra. So after initialization in your viewModel you have set parameter you can observe using liveData. I am suing Kotlin in answer.
as example:
override fun onCreateView(
........
val viewModelFactory =
TestViewModelFactory(
yourINFOparameter,
application)
val testViewModel = ViewModelProvider(
this, viewModelFactory
).get(TestViewModel::class.java)
.................
Then declare class TestViewModelFactory:
class TestViewModelFactory(
private val yourINFOparameter: String, private val application: Application) : ViewModelProvider.Factory {
#Suppress("unchecked_cast")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(TestViewModel::class.java)) {
return TestViewModel(yourINFOparameter,
application) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
And your viewModelclass:
class TestViewModel(yourINFOparameter: String, application: Application) : AndroidViewModel(application) {
............................}
I am trying to get a value from the SharedViewModel class but the ViewModelProvider() is giving a parameter error when i am passing requireActivity() although the same initilization and assignment works in my fragments.
It is requiring "ViewModelStoreOwner" to be passed.
class CourseRepository(val app: Application) {
private var viewModel: SharedViewModel = ViewModelProvider(requireActivity()).get(SharedViewModel::class.java)
val courseData = MutableLiveData<List<Course>>()
init {
CoroutineScope(Dispatchers.IO).launch {
callWebService()
}
}
#WorkerThread
suspend fun callWebService() {
if (Utility.networkAvailable(app)) {
val retrofit = Retrofit.Builder().baseUrl(WEB_SERVICE_URL).addConverterFactory(MoshiConverterFactory.create()).build()
val service = retrofit.create(CourseService::class.java)
val serviceData = service.getCourseData(viewModel.pathName).body() ?: emptyList()
courseData.postValue(serviceData)
}
}
}
The purpose of the ViewModel here is because i am storing the Id of the selected RecyclerView item in order to send it to a server
ViewModel instances are scoped to Fragments or Activities (or anything with a similar lifecycle), which is why you need to pass in a ViewModelStoreOwner to the provider to get a ViewModel from it. The point of ViewModels is that they will exist until the store they belong to is destroyed.
The requireActivity method doesn't work here, because you're not inside a Fragment.
Some things to consider here:
Do you really need ViewModel in this use case? Could you perhaps use just a regular class that you can create by calling its constructor?
Could you call this Repository from your ViewModel, and pass in any parameters you need from there?
According to the section "Share data between fragments" at https://developer.android.com/topic/libraries/architecture/viewmodel we are told that creating a ViewModel in the activity scope and sharing that amongst the fragments is the way to go.
This is the Fragment which sets the value in the ViewModel
class MasterFragment : Fragment() {
private lateinit var itemSelector: Selector
private lateinit var model: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model = activity?.run {
ViewModelProviders.of(this).get(SharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
itemSelector.setOnClickListener { item ->
// Update the UI
}
}
}
This is the detail fragment which uses the property set
class DetailFragment : Fragment() {
private lateinit var model: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model = activity?.run {
ViewModelProviders.of(this).get(SharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
model.selected.observe(this, Observer<Item> { item ->
// Update the UI
})
}
}
This is the ViewModel
class SharedViewModel : ViewModel() {
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
}
My question is simple. Assuming the MasterFragment set a value in the ViewModel on a button click, how would we recover that value when accessing it AFTER THE SYSTEM HAS KILLED OUR APPLICATION AND RESTARTED IT ?.
Our DetailFragment will not be seeing the value since we were setting it on the button click in the MasterFragment. To understand the question better, consider we have Fragment A, B, C, and D and they share a ViewModel which has a value Fragment A B and C together computed and placed it in ViewModel for Fragment D to access.
Now when the system kills and recreates our application Fragment D won't have that value available.
OnSaveInstance also won't be able to help out much without resorting to dirty code. For simple situations, yes , but like the one in which FragmentA B and C together are making a value, in that situation, OnSaveInstance would be problematic.
OnSaveInstance should have been inside the ViewModel but alas I don't think that's the case. Any ideas?
ViewModel objects are scoped to the Lifecycle passed to the ViewModelProvider when getting the ViewModel. The ViewModel remains in memory until the Lifecycle it's scoped to goes away permanently: in the case of an activity, when it finishes, while in the case of a fragment, when it's detached.
You can check it here
My question is simple. Assuming the MasterFragment set a value in the ViewModel on a buttonClick , how would we recover that value when accessing it AFTER THE SYSTEM HAS KILLED OUR APPLICATION AND RESTARTED IT ?.
You can't recover the value if the application is killed by the user or system or restarted.
To solve your purpose of accumulating data from Activity A, B and C and display it in Activity D even though the application is killed or restarted, you can choose any 1 method from the following:
1. SharedPreference
2. Local Database Room or SQLite
3. Store data in a file
I recommend you to use SharedPreference for small data and Room for Large and Complex data.
In a nutshell, ViewModel stores data temporary to survive orientation change(no need to write code of onSaveInstanceState and onRestoreInstanceState) and share data between Activities and Fragments. Data will be lost if the activity is destroyed or fragment is detached.
If you still want to get stored value after app reset or killed you need to save data to SharedPreferences or internal SqLite database and restore it after app start.
For those using Kotlin out there try the following approach:
Add the androidx ViewModel and LiveData libraries to your gradle file
Call your viewmodel inside the fragment like this:
class MainFragment : Fragment() {
private lateinit var viewModel: ViewModel
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// kotlin does not have a getActivity() built in method instead we use activity, which is null-safe
activity?.let {
viemModel = ViewModelProvider(it).get(SharedViewModel::class.java)
}
}
}