How to keep Kotlin property syntax when defining an Interface? - android

CLARIFICATION: property syntax == val s = supportFragmentManager as opposed to val s = getSupportFragmentManager()
If I'm writing an interface for an Activity class and want to expose a property while retaining the property syntax, how do I do it?
Create a class inheriting AppCompatActivity and implements the following interface.
interface MyInterface {
fun getSupportFragmentManager: FragmentManager // Option 1 Boo!
val supportFragmentManager: FragmentManager // Option 2 Yey!
}
Option 1 will work just fine.
MyActivity already contains a function called getSupportFragmentManager() so I don't have to implement it.
Option 2 will prompt me to implement the missing property, and doing so I will result in the following
class MyActivity: AppCompatActivity(), MyInterface {
override val supportFragmentManager: FragmentManager
get() = TODO("Not yet implemented")
}
This will give me an error since there already is a function with the same signature in the class.
Accidental override: The following declarations have the same JVM signature (getSupportFragmentManager()Landroidx/fragment/app/FragmentManager;):
fun <get-supportFragmentManager>(): FragmentManager defined in com.my.project.MyActivity
fun getSupportFragmentManager(): FragmentManager defined in com.my.project.MyActivity
Any ides of how to keep Kotlins property syntax throughout interfaces?

I just checked the compiler warnings, and the Accidental override exception you're getting is because of defining both the get function and the original variable itself.
You could do it like this:
interface MyInterface {
val supportFragmentManager: FragmentManager
}
class MyActivity: AppCompatActivity(), MyInterface {
override val supportFragmentManager: FragmentManager = FragmentManager(...)
get() = ... //logic (getter is optional, by default kotlin generates getter itself)
}
PS: In getter you can use field to access the variable aka supportFragmentManager like
get() = field.apply { updateSomthing() }
this calls MyActivity.updateSomthing() and return field itself.

Related

Using interface in Fragment - android kotlin

I create a Interface like bellow:
interface LevelsViewListener {
fun isVisible()
}
and in my activity is:
class MainActivity : AppCompatActivity(){
var levelsViewListener: LevelsViewListener? = null
fun initTEST(listener: LevelsViewListener){
this.levelsViewListener = listener
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
(context as MainActivity).levelsViewListener
btnClick.....{
levelsViewListener!!.isVisible()
}
}
}
And in my fragment is:
class LessonsFragment : Fragment(), LevelsViewListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
(activity as MainActivity?)?.initTEST(this#LessonsFragment)
}
}
But get me bellow error in LessonsFragment :
If a class implements two interfaces (or, let's say, implements one interface and extends one class, as in your case), and each interface defines a method that has an identical signature, then in fact there is only one method, and they are not distinguishable. If, say, two methods have conflicting return types, then it will be a compilation error. This is what happens in your fragment class.
The problem is that the Fragment class also has an isVisible function, but it returns boolean. And your isVisible function in LevelsViewListener returns Unit.
There are two solutions to this problem.
You should return boolean in your isVisible function in LevelsViewListener interface.
You can change the name of the function in LevelsViewListener interface
The problem is that isVisible() function is already defined as part of the Fragment class. As reported in the error message the signature is:
final public boolean isVisible()
and its return value is boolean. Conversely, your interface LevelsViewListener defines a method with the same name but without return value. This scenario is not allowed by Java/Kotlin.

ClassCastException When Initialising Interface ViewModel Using by navGraphViewModels() [with Hilt]

I have a Fragment flow scoped with a navigation graph and want to scope each Fragment's ViewModel accordingly. However, I don't want each of the Fragments to have access to all methods and variables in the ViewModel, therefore each Fragment's ViewModel is an interface implemented in the base ViewModel.
I am using by navGraphViewModels() delegation in each of the Fragments but it seems to be unable to cast the interface to the base class.
The trace error is:
java.lang.ClassCastException: java.lang.Object cannot be cast to
androidx.lifecycle.ViewModel
Any advice on how to approach this problem??
In my Fragment it is defined as follows:
#AndroidEntryPoint
class ExampleFragment : Fragment() {
private val viewModel: ExampleViewModelController by
navGraphViewModels(R.id.nav_graph_example){defaultViewModelProviderFactory}
///
And the ViewModel is defined by:
#HiltViewModel
class ExampleViewModel #Inject constructor(
private val handle: SavedStateHandle,
private val useCases: ExampleUseCases,
) : ViewModel(), ExampleViewModelController {
override fun validateExampleInputs() {
// TODO("Not yet implemented")
}
}
And lastly, the interface:
interface ExampleViewModelController {
fun validateExampleInputs()
}
The ClassCastException happens because there's no type parameter passed to the delegate like by navGraphViewModels<ExampleViewModel>(). Thus, the delegate is wrongly trying to create a new instance of the interface ExampleViewModelController instead of ExampleViewModel.

Type cast / make genric viewModel to be passed as parameter

I am working on a custom dialog fragment, that is being used/ called from two different views having different viewModels. Instead of passing two separate viewModels in the constructor parameter of Dialog class as,
class CustomeDialog(var viewModel1: ViewModelA ?= null, var viewModel2 : ViewModelB ?= null) : DialogFragment()
I need to ask/ figure out a way where I could just set < T> kind of parameter to dialog so I could just type caste any viewModel to it, I want.
something like this,
class CustomDialog<T:ViewModel> : DialogFragment()
and in code, it would be something like
val mdialog1: CustomeDialog by lazy { CustomeDialog(viewModel as ViewModelA) }
and also
val mdialog2: CustomeDialog by lazy { CustomeDialog(viewMode2 as ViewModelB) }
You can create a secondary constructor in the generic class that takes in a generic ViewModel parameter:
class CustomeDialog<T : ViewModel>() : DialogFragment() {
constructor(viewmodel: T) : this()
}
And the usage the same as you did:
lateinit var viewModel: ViewModel
val mdialog1: CustomeDialog<ViewModelA> by lazy { CustomeDialog(viewModel as ViewModelA) }
lateinit var viewModel2: ViewModelA
val mdialog2: CustomeDialog<ViewModelA> by lazy { CustomeDialog(viewModel2) }
UPDATE:
how to initialize viewModel in dialog based on the type. eg. if VM1 is passed in constructor, then var dialogViewModel = WHAT??,
It's requested to have a Dialog with a generic ViewModel, so its type is Generic as it's unknown till it's instantiated.
yeah i need a local var dialogViewModel which is generic, as i mentioned, whole logic is dependent on this dvm
You can initialize it in the secondary constructor:
class CustomDialog<T : ViewModel>() : DialogFragment() {
lateinit var dialogViewModel: T
constructor(viewmodel: T) : this() {
dialogViewModel = viewmodel
}
}
This strategy cannot work. The OS recreates your Fragment using reflection and its empty constructor. It can restore state to the replacement Fragment using Bundle values, but class types are not a valid type of data for a Bundle.
Closest I can come up with is to make it an abstract class, and then create simple subclasses that have concrete types.
abstract class CustomDialog<T: ViewModel>(viewModelType: KClass<out T>): DialogFragment() {
val viewModel: T by createViewModelLazy(viewModelType, { viewModelStore })
}
class CustomDialogA: CustomDialog<ViewModelA>(ViewModelA::class)
class CustomDialogB: CustomDialog<ViewModelB>(ViewModelB::class)

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 Dagger 2 to Inject ViewModel of same Fragments inside ViewPager

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

Categories

Resources