Passing complex objects as a parameter in Android's Navigation Architecture Component - android

I've been looking for some time to android architechture components and lately to the Navigation component.
I'm trying to pass as a parameter an object, so the next fragment can retrieve that data, but to do so I'm required to do one of two things:
Pass it through a Bundle, which will make me implement the Parcelable interface to that object.
Use the "Safeargs" plugin which I've tried and it looks like behind the hood makes use of Bundles and requires the implementation of the Parcelable interface anyway.
The thing about these options is that I've read that Parcelable makes use of reflection and it can get quite expensive regarding time
I have also tried to build a "SharedMasterDetailsViewModel" but with no luck since the observable callbacks are not being performed on my newly created Fragment. (I think LiveData performs the callback before my fragment is created)
Here's some code about how I've tried to approach this
SharedSessionViewModel
class SessionSharedViewModel : ViewModel() {
var sharedSession: LiveData<Session> = MutableLiveData()
private set
fun setSession(data: Session) {
val casted = this.sharedSession as MutableLiveData<Session>
casted.postValue(data)
}
}
MasterFragment
override fun onItemClicked(item: Session) {
sessionSharedViewModel.setSession(item) // Item is a complex object of mine
this#HomeFragment.findNavController().navigate(R.id.sessionDetailsFragment)
}
DetailsFragment
class SessionDetailsFragment : Fragment() {
companion object {
fun newInstance() = SessionDetailsFragment()
}
private lateinit var sharedViewModel: SessionSharedViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.session_details_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
Log.d("SESSIONDETAILS","created!")
super.onActivityCreated(savedInstanceState)
sharedViewModel = ViewModelProviders.of(this).get(SessionSharedViewModel::class.java)
sharedViewModel.sharedSession.observe({this.lifecycle},{ handleUI(it!!)})
}
fun handleUI(sharedSession: Session) {
Toast.makeText(activity, "This is inside new activity: ${sharedSession.title()}", Toast.LENGTH_SHORT)
}
}
My last hope is to serialize my object into a JSON string and reparse that object on the onCreateActivity lyfecycle hook of my Detail fragment but I feel like that is not the proper solution.
In the worst case scenerio I would just pass the id of the object and re-fetch it from the network, but since I already have the info I want to show I'd like to pass it as a parameter.

TL; DR
You can't.
Actual explanation
First thing; the following statement
The thing about these options is that I've read that Parcelable makes use of reflection and it can get quite expensive regarding time.
IS A LIE
Since you implement Parcelable you're just providing methods on how to serialize and deserialize using basic primitive types: IE: writeBytes, readBytes, readInt, writeInt.
Parcelable does NOT use reflection. Serializable does!
While it's true you are forced to use Parcelable jet brains developed a very useful annotation that takes away the pain of having to write the parcelable implementation called #Parcelize.
Sample usage:
#Parcelize
data class User(val username: String, val password: String) : Parcelable
And now you're able to pass instances of the class without writing a single line of Parcelable implementation.
More info here

Related

Can pass data from View to ViewModel?

Is there mistake if I pass data from View to ViewModel? For example, pass url from onPageFinished event of WebView. I am confused because all source tell that ViewModel mustn't have any link to View. In this case will be such link or not? Or if type of argument will be custom data class than just string?
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.webView.settings.javaScriptEnabled = true
binding.webView.webViewClient = object : WebViewClient(){
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
viewModel.onPageFinished(url) // this line
}
}
viewModel.url.observe(this) { url ->
binding.webView.loadUrl(url)
}
}
}
class MainViewModel: ViewModel() {
private val _cookieManager: CookieManager
private lateinit var _url: MutableLiveData<String>
val url: LiveData<String> = _url
init {
_url.value = "google.com"
_cookieManager = CookieManager.getInstance()
}
fun onPageFinished(url: String) {
val cookies = _cookieManager.getCookie(url)
Log.i("MainViewMovel", url)
Log.i("MainViewMovel", cookies)
}
}
The View is the UI, it has to pass some data to the View Model, like button presses, typed search queries, requests for data when the user scrolls etc. It's absolutely a two-way thing - here's the docs talking about the recommended way of designing your app. There's more on the side, including a section on the UI Layer (the View)
The terminology might be a little confusing here, because the View in Model-View-ViewModel is referring to your UI layer. But a View in Android is a layout component, which is generally tied to a specific Fragment or Activity which have shorter lifetimes than a ViewModel (one of the main points of a VM is to retain data even when Activity and Fragment instances are destroyed and recreated).
So for that reason, your ViewModel shouldn't hold any references to any Views. That's why you expose data through things like LiveData which are observed by passing a LifecycleOwner - the observers are automatically removed when they're destroyed, so the LiveData in the VM isn't keeping them alive
As far as your question goes, I don't think it hugely matters - your WebViewClient is a little bit of wiring between the View and the ViewModel, and there's always a bit of that! But if you wanted, you could always put the WebViewClient in the ViewModel, there's nothing obviously tying it to a particular view
I think that makes more sense in general - if you look at the other callbacks in WebViewClient, a lot of them are about program logic, handling events and situations. The UI layer shouldn't really be concerned with any of that, so it makes more sense to me to have the VM take care of that, decide what needs to happen, and just push any resulting UI state updates to the UI.
An alternative to keeping a singleton WebViewClient in the VM would be to have a function in there that creates a new one and configures it. That way you don't need to worry about it having any state relating to the old WebView, but all the code is still internal to the ViewModel:
class MainViewModel: ViewModel() {
...
fun getWebViewClient() = object : WebViewClient(){
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
onPageFinished(url) // calling the internal VM method
}
}
}
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
binding.webView.webViewClient = viewModel.getWebViewClient()

How to save the data of recyclerview LiveData<List<SportData>> to avoid the loss of bottom navigation data?

The data in the recyclerview in my fragment uses the api of the website. But the data is lost when the bottom navigation is switched. But the data structure is not a simple int or string. How should I write it in onSaveInstanceState to store it. And how to make him restore the data type of LiveData<List> normally?
The data of the recyclerview looks like in the viewmodel.
private val _photos = MutableLiveData<List<SportData>>()
val photos: LiveData<List<SportData>> get() = _photos
data class
#Parcelize
data class SportData (
val GymID:Int,
val Photo1:String,
val Name:String,
val Address:String,
val OperationTel:String,
val OpenState:String,
val GymFuncList:String
): Parcelable
I try to save the data in onDestroyView() and fetch it in onViewCreated. It fails with null.
override fun onDestroyView() {
photos2=viewModel.photos
super.onDestroyView()
Log.d("aaa","destroyVIEW and ${photos2}")
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.d("aaa","=viewCreate and ${photos2}")
if (photos2!=null){
viewModel.saveData(photos2!!)
}
viewmodel
fun saveData(savePhoto:LiveData<List<SportData>>){
_photos=savePhoto as MutableLiveData<List<SportData>>
}
hello can you help me? thanks
Your data is already at ViewModel. You don't need to save it. Since data in your viewModel and viewModel lives while your aplication lives, you'll not lose it.
What might be happening is a reload when you go back to your list's fragment, right?
You are calling a viewModel method from your fragment. This method does the request.
What you need to do is to make sure your fragment won't call it if it doesn't need.
What you need to do is:
if (savedBundleState == null) { //Read this as android creating this frag for the very first time
//Here you call viewmodel method that does the request.
}
This is part 1
Since you are using Navigation Component, you'll need to setup it to avoid new fragments killing older fragments.

Android, Koin: How to prevent interface bound by viewModel from creating new viewModel instance?

It's hard to understand what the problem is from the headline - I'll try my best explaining:
I'm using Koin for dependency injection. I'm injecting my HomeViewModelinto my HomeFragment (the viewModel has parameters, but that should be unrelated to the problem):
// fragment code
private var viewModelParameters: ParametersDefinition? = null
lateinit var viewModel: VM
...
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, layout, container, false)
binding?.lifecycleOwner = viewLifecycleOwner
viewModel = getViewModel(HomeViewModel::class, parameters = viewModelParameters)
return binding?.root ?: inflater.inflate(layout, container, false)
}
The fragment contains a RecyclerView. The recycler's ViewHolder declares an interface, that is injected via Koins by inject()`:
class MyRecyclerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), KoinComponent {
private val callback by inject<Callback>()
fun bind(item: MyItemType) {
itemView.setOnClickListener { callback.myCallbackFunction(item) }
}
interface Callback {
fun myCallbackFunction(item: MyItemType)
}
}
My HomeviewModel implements this interface, and I bind it to the viewModel in my KoinGraph module via Koin's bind DSL method:
private val baseModule = module {
single { androidApplication().resources }
single { PermissionHelper(androidApplication()) }
...
viewModel { HomeViewModel() } bind MyRecyclerviewHolder.Callback::class
}
Now, when I click on my recycler item, the callback's myCallBackFunction is called, which should trigger the implementation in my HomeViewModel. Which it does, but: It is not the same instance, but a new HomeViewmodel.
My understanding is that Android's ViewModelclass, if used in the typical way (currently using, without Koin, by viewModels() - see here), should only exist once. But with Koin's viewModel{} call, I can create multiple instances, which I think I shouldn't be able to? Or should I (and if yes, why)?
Anyway, I'd like to bind my callback to the view model I already have (the one the fragment knows of) and not a new instance my fragment doesn't know about.
How can I achieve that using Koin and its injection patterns?
By the way, If I use
single { HomeViewModel() } bind MyRecyclerviewHolder.Callback::class
instead of
viewModel { HomeViewModel() } bind MyRecyclerviewHolder.Callback::class
my code works as intended - since I'm forcing my view model to be a singleton that way - which is what I want. But what is the point of the viewModel{} command then? And are there any downsides to it? It doesn't feel like what I should be supposed to do but maybe it's totally fine?

How can I initialize an androidx ViewModel from parcelable data?

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)

How to use android navigation without binding to UI in ViewModel (MVVM)?

I am using android navigation that was presented at Google I/O 2018 and it seems like I can use it by binding to some view or by using NavHost to get it from Fragment. But what I need is to navigate to another specific view from ViewModel from my first fragment depending on several conditions. For ViewModel, I extend AndroidViewModel, but I cannot understand how to do next. I cannot cast getApplication to Fragment/Activity and I can't use NavHostFragment. Also I cannot just bind navigation to onClickListener because the startFragment contains only one ImageView. How can I navigate from ViewModel?
class CaptionViewModel(app: Application) : AndroidViewModel(app) {
private val dealerProfile = DealerProfile(getApplication())
val TAG = "REGDEB"
fun start(){
if(dealerProfile.getOperatorId().isEmpty()){
if(dealerProfile.isFirstTimeLaunch()){
Log.d(TAG, "First Time Launch")
showTour()
}else{
showCodeFragment()
Log.d(TAG, "Show Code Fragment")
}
}
}
private fun showCodeFragment(){
//??
}
private fun showTour(){
//??
}
}
My Fragment
class CaptionFragment : Fragment() {
private lateinit var viewModel: CaptionViewModel
private val navController by lazy { NavHostFragment.findNavController(this) }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
viewModel = ViewModelProviders.of(this).get(CaptionViewModel::class.java)
return inflater.inflate(R.layout.fragment_caption, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel.start()
}
}
I want to keep logic of navigation in ViewModel
How can I navigate from ViewModel?
The answer is please don't. ViewModel is designed to store and manage UI-related data.
New Answer
In my previous answers, I said that we shouldn't navigate from ViewModel, and the reason is because to navigate, ViewModel must have references to Activities/Fragments, which I believe (maybe not the best, but still I believe it) is never a good idea.
But, in recommended app architecture from Google, it mentions that we should drive UI from model. And after I think, what do they mean with this?
So I check a sample from "android-architecture", and I found some interesting way how Google did it.
Please check here: todo-mvvm-databinding
As it turns out, they indeed drive UI from model. But how?
They created an interface TasksNavigator that basically just a navigation interface.
Then in the TasksViewModel, they have this reference to TaskNavigator so they can drive UI without having reference to Activities / Fragments directly.
Finally, TasksActivity implemented TasksNavigator to provide detail on each navigation action, and then set navigator to TasksViewModel.
You can use an optional custom enum type and observe changes in your view:
enum class NavigationDestination {
SHOW_TOUR, SHOW_CODE_FRAGMENT
}
class CaptionViewModel(app: Application) : AndroidViewModel(app) {
private val dealerProfile = DealerProfile(getApplication())
val TAG = "REGDEB"
private val _destination = MutableLiveData<NavigationDestination?>(null)
val destination: LiveData<NavigationDestination?> get() = _destination
fun setDestinationToNull() {
_destination.value = null
}
fun start(){
if(dealerProfile.getOperatorId().isEmpty()){
if(dealerProfile.isFirstTimeLaunch()){
Log.d(TAG, "First Time Launch")
_destination.value = NavigationDestination.SHOW_TOUR
}else{
_destination.value = NavigationDestination.SHOW_CODE_FRAGMENT
Log.d(TAG, "Show Code Fragment")
}
}
}
}
And then in your view observe the viewModel destination variable:
viewModel.destination.observe(this, Observer { status ->
if (status != null) {
viewModel.setDestinationToNull()
status?.let {
when (status) {
NavigationDestination.SHOW_TOUR -> {
// Navigate to your fragment
}
NavigationDestination.SHOW_CODE_FRAGMENT -> {
// Navigate to your fragment
}
}
})
}
If you only have one destination you can just use a Boolean rather than the enum.
There are two ways I can recommend doing this.
Use LiveData to communicate and tell the fragment to navigate.
Create a class called Router and this can contain your navigation logic and reference to the fragment or navigation component. ViewModel can communicate with the router class to navigate.

Categories

Resources