setOnCreatePreferencesChangeListener Type Mismatch - android

My kotlin fragment class:
class OptionsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceChangeListener {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.options, rootKey)
val contact = findPreference("contact_developer")
contact.setOnPreferenceChangeListener(this)
}
The this in contact.setOnPreferenceChangeListener(this) is underlined red and showing this error:
Type Mismatch.
Required: Preferences.OnPreferenceChangeListener!
Found: OptionsFragment
My fragment clearly implements Preference.OnPreferenceChangeListener so why am I getting this error?

I think your implementation is wrong
implement Preferences.OnPreferenceChangeListener instead of Preference.OnPreferenceChangeListener

Related

How to open a new PreferenceFragment from current one, using the new Android-X API?

Background
On previous versions of support library, we could use headers in order to have a main-menu screen of settings, that each would open a new settings screen (fragment) .
The problem
Now headers are gone (as written here) for some time, and I think it became worse on android-x :
One thing you’ll note isn’t in here is preference headers and you’d be
totally right. However, that doesn’t mean a single list of preferences
need to span a 10” tablet screen. Instead, your Activity can implement
OnPreferenceStartFragmentCallback (link) to handle
preferences with an app:fragment attribute or
OnPreferenceStartScreenCallback (link) to handle
PreferenceScreen preferences. This allows you to construct a ‘header’
style PreferenceFragmentCompat in one pane and use those callbacks to
replace a second pane without working in two separate types of XML
files.
Thing is, I fail to use these on the new android-x API.
Each fragment has its own preferences XML tree (using setPreferencesFromResource within onCreatePreferences) , but each solution I've come up with has either done nothing, or crashed.
To put it in a visual way, this is what I'm trying to achieve :
Since there are multiple sub settings screens, it would be very messy to have all of the preferences of all of them be put in one XML file of the main settings screen.
What I've tried
Only thing I've succeeded, is to use the PreferenceScreen to hold the preferences of the sub-screen that's supposed to be shown.
Here's a working code (project available here) of such a thing :
preferences.xml
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" android:title="Demo">
<PreferenceScreen
android:key="screen_preference" android:summary="Shows another screen of preferences"
android:title="Screen preferenc">
<CheckBoxPreference
android:key="next_screen_checkbox_preference"
android:summary="Preference that is on the next screen but same hierarchy"
android:title="Toggle preference"/>
</PreferenceScreen>
</PreferenceScreen>
MainActivity.kt
class MainActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartScreenCallback {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
if (savedInstanceState == null)
supportFragmentManager.beginTransaction().replace(android.R.id.content, PrefsFragment()).commit()
}
override fun onPreferenceStartScreen(caller: PreferenceFragmentCompat, pref: PreferenceScreen): Boolean {
val f = PrefsFragment()
val args = Bundle(1)
args.putString(PreferenceFragmentCompat.ARG_PREFERENCE_ROOT, pref.key)
f.arguments = args
supportFragmentManager.beginTransaction().replace(android.R.id.content, f).addToBackStack(null).commit()
return true
}
class PrefsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey)
}
}
}
But, as I wrote, this is not what I'm trying to do. I want to have multiple classes that extend PreferenceFragmentCompat, each with its own XML file, which will be opened from the main one.
Here are the things I've tried (and failed) :
Set a "android:fragment" for the PreferenceScreen, to point to the new fragments classes, similar to headers. This didn't do anything at all.
Use a normal Preference and have click listener for it, that will do the fragment transaction as shown on the original code. This caused a crash that says something like "Preference object with key screen_preference is not a PreferenceScreen" .
Tried to avoid using ARG_PREFERENCE_ROOT , but had same crash as on #2 .
As suggested here, I tried to return this in function getCallbackFragment, but this didn't help at all.
The question
Is it possible to have the main settings fragment just let the user to navigate to the other fragments, while not having any other preferences that belong to them (inside preferences.xml) ?
How?
What you tried in 1) was the correct approach - but you should not use <PreferenceScreen> tags for this.
Your XML resource should look like this instead:
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
app:key="screen_preference"
app:summary="Shows another screen of preferences"
app:title="Screen preference"
app:fragment="com.example.user.myapplication.MainActivity$PrefsFragment2"/>
</PreferenceScreen>
Also, if you are using a version of Preference older than androidx.preference:preference:1.1.0-alpha01, you will need to implement onPreferenceStartFragment to handle the fragment transaction. (in 1.1.0 alpha01 this method has a default implementation, but you are still encouraged to use your own implementation to customize any animations / transitions)
This should look something like:
override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat,
pref: Preference
): Boolean {
// Instantiate the new Fragment
val args = pref.extras
val fragment = supportFragmentManager.fragmentFactory.instantiate(
classLoader,
pref.fragment,
args
).apply {
arguments = args
setTargetFragment(caller, 0)
}
// Replace the existing Fragment with the new Fragment
supportFragmentManager.beginTransaction()
.replace(R.id.settings, fragment)
.addToBackStack(null)
.commit()
return true
}
For more information you can check out the Settings guide
and the AndroidX Preference Sample
EDIT: a sample of the first solution, after updating, available here.
Here's how it can work (sample available here) :
MainActivity.kt
class MainActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat, pref: Preference): Boolean {
//Note: this whole function won't be needed when using new version of fragment dependency (1.1.0 and above)
val fragment = Fragment.instantiate(this, pref.fragment, pref.extras)
fragment.setTargetFragment(caller, 0)
supportFragmentManager.beginTransaction().replace(android.R.id.content, fragment).addToBackStack(null).commit()
return true
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
if (savedInstanceState == null)
supportFragmentManager.beginTransaction().replace(android.R.id.content, PrefsFragment()).commit()
}
class PrefsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey)
}
}
class PrefsFragment2 : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences2, null)
}
}
}
preferences.xml
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
app:fragment="com.example.user.myapplication.MainActivity$PrefsFragment2" app:key="screen_preference" app:summary="Shows another screen of preferences"
app:title="Screen preference"/>
</PreferenceScreen>
preferences2.xml
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" android:title="Demo">
<PreferenceCategory android:title="Category">
<CheckBoxPreference
android:key="next_screen_checkbox_preference" android:summary="AAAA" android:title="Toggle preference"/>
</PreferenceCategory>
</PreferenceScreen>
gradle dependencies:
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.preference:preference:1.0.0'
OK, I've found 2 possible, yet weird, solutions.
I still would like to know if there is an official way to do it, because both solutions are quite weird.
Solution 1
In the main settings preference XML file, for each sub PreferenceScreen, I put an empty Preference tag.
preferences.xml
<PreferenceScreen
android:key="screen_preference" android:summary="Shows another screen of preferences"
android:title="Screen preference">
<Preference/>
</PreferenceScreen>
I pass null for the second argument of setPreferencesFromResource on the new sub-screen fragment.
Here's the code (project available here) :
MainActivity.kt
class MainActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartScreenCallback {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
if (savedInstanceState == null)
supportFragmentManager.beginTransaction().replace(android.R.id.content, PrefsFragment()).commit()
}
override fun onPreferenceStartScreen(caller: PreferenceFragmentCompat, pref: PreferenceScreen): Boolean {
supportFragmentManager.beginTransaction().replace(android.R.id.content, PrefsFragment2()).addToBackStack(null).commit()
return true
}
class PrefsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey)
}
}
class PrefsFragment2 : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences2, null)
}
}
}
Of course, this needs to be modified so that you will know which fragment to create and add...
Solution 2
I use a normal Preference instead of each PreferenceScreen, and for each of them I choose to add the fragment upon clicking (project available here) :
preferences.xml
<Preference
android:key="screen_preference" android:summary="Shows another screen of preferences"
android:title="Screen preference"/>
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
if (savedInstanceState == null)
supportFragmentManager.beginTransaction().replace(android.R.id.content, PrefsFragment()).commit()
}
class PrefsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey)
setPreferenceToOpenFragmentAsNewPage(findPreference("screen_preference"), PrefsFragment2::class.java)
}
private fun setPreferenceToOpenFragmentAsNewPage(pref: Preference, java: Class<out PreferenceFragmentCompat>) {
pref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
val fragment = java.newInstance()
val args = Bundle(1)
args.putString(PreferenceFragmentCompat.ARG_PREFERENCE_ROOT, pref.key)
fragment.arguments = args
activity!!.supportFragmentManager.beginTransaction().replace(android.R.id.content, fragment).addToBackStack(null).commit()
true
}
}
}
class PrefsFragment2 : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences2, null)
}
}
}
EDIT: a tiny modification to the second solution can make it nicer:
preferences.xml
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" android:title="Demo">
<Preference
android:fragment="com.example.user.myapplication.MainActivity$PrefsFragment2" android:key="screen_preference"
android:summary="Shows another screen of preferences" android:title="Screen preference"/>
</PreferenceScreen>
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
if (savedInstanceState == null)
supportFragmentManager.beginTransaction().replace(android.R.id.content, PrefsFragment()).commit()
}
class PrefsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey)
setPreferenceToOpenFragmentAsNewPage(findPreference("screen_preference"))
}
private fun setPreferenceToOpenFragmentAsNewPage(pref: Preference) {
pref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
val clazz = Class.forName(pref.fragment)
val fragment: PreferenceFragmentCompat = clazz.newInstance() as PreferenceFragmentCompat
val args = Bundle(1)
args.putString(PreferenceFragmentCompat.ARG_PREFERENCE_ROOT, pref.key)
fragment.arguments = args
activity!!.supportFragmentManager.beginTransaction().replace(android.R.id.content, fragment).addToBackStack(null).commit()
true
}
}
}
class PrefsFragment2 : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences2, null)
}
}
}
Note that you need to add this to Proguard rules:
-keepnames public class * extends androidx.preference.PreferenceFragmentCompat
Another improvement to solution #2 is that it can go over all preferences by itself:
class PrefsFragment : BasePreferenceFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences_headers, rootKey)
val preferenceScreen = preferenceScreen
val preferenceCount = preferenceScreen.preferenceCount
for (i in 0 until preferenceCount) {
val pref = preferenceScreen.getPreference(i)
val fragmentClassName = pref.fragment
if (fragmentClassName.isNullOrEmpty())
continue
pref.setOnPreferenceClickListener {
showPreferenceFragment(activity!!, fragmentClassName)
true
}
}
}
}
companion object {
#JvmStatic
private fun showPreferenceFragment(activity: FragmentActivity, fragmentClassName: String) {
val clazz = Class.forName(fragmentClassName)
val fragment: PreferenceFragmentCompat = clazz.newInstance() as PreferenceFragmentCompat
val fragmentsCount = activity.supportFragmentManager.fragments.size
val transaction = activity.supportFragmentManager.beginTransaction().replace(android.R.id.content, fragment)
if (fragmentsCount > 0)
transaction.addToBackStack(null)
transaction.commit()
}
}
EDIT: seems the first solution was the correct one, but needed a change. Check the answer here. Full sample available here.

When I use ViewModel.Factory, appear `Kodein No binding found for bind<Kodein>()` error

I reference to http://kodein.org/Kodein-DI/?6.1/android#view-model-factory use the kodein viewmodel, appeared the error.
the part code
bind<ViewModelProvider.Factory>() with singleton { KodeinViewModelFactory(instance()) }
class KodeinViewModelFactory(private val kodein: Kodein) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T =
kodein.direct.Instance(TT(modelClass))
}

How to have generic ViewModel in BaseActivty class

I want to have a base activity class that takes care of some initialization I started defining it like this.
abstract class BaseActivity<VIEW_MODEL : ViewModel, BINDING : ViewDataBinding> :
AppCompatActivity() {
lateinit var viewmodel: VIEW_MODEL
lateinit var binding: BINDING
lateinit var glide: RequestManager
#get:LayoutRes
abstract val layoutResource: Int
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, layoutResource)
viewmodel = ViewModelProviders.of(this).get(VIEW_MODEL::class.java)
^^^^^^^^^^^^^^^^^
this is causing error
}
}
But I am getting this error
Cannot use 'VIEW_MODEL' as reified type parameter. Use a class instead
How should I solve this problem, where I want to define the type of ViewModel as typed parameter of BaseActivity.
And how can I instantiate it?
one solution is to have something like this
abstract val viewModelClass: Class<VIEW_MODEL>
I would really like not to have something like this
#svkaka already mention a solution. Create an abstract method and override it with your ViewModel class:
protected abstract fun getViewModel(): Class<M>
Replace your line by
viewmodel = ViewModelProviders.of(this).get(getViewModel())
Hopefully, it will Works.
1st way Make abstarct method in Base Activity.In this way you have to override abstarct method in all activity.
abstract class BaseActivity< ViewModel : BaseViewModel,Binding : ViewDataBinding> : AppCompatActivity(){
protected lateinit var binding: Binding
private var mViewModel: V? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
performViewModelBinding()
}
#LayoutRes
abstract fun getLayoutResId(): Int
abstract fun getViewModel(): ViewModel
private fun performViewModelBinding() {
binding = DataBindingUtil.setContentView(this, getLayoutResId())
this.mViewModel = mViewModel ?: getViewModel()
binding.setVariable(BR.viewModel, mViewModel)
binding.executePendingBindings()
}
}
And when you override this method in activity
override fun getViewModel(): MyProfileViewModel {
mMyProfileViewModel = ViewModelProviders.of(this, mViewModelFactory).get(MyProfileViewModel::class.java)
return mMyProfileViewModel
}
2nd way you can do this
abstract class BaseActivity<VIEW_MODEL : ViewModel, BINDING : ViewDataBinding> :
AppCompatActivity() {
lateinit var viewmodel: VIEW_MODEL
lateinit var binding: BINDING
lateinit var glide: RequestManager
#get:LayoutRes
abstract val layoutResource: Int
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, layoutResource)
viewmodel = ViewModelProviders.of(this).get(getViewModelClass())
}
private fun getViewModelClass(): Class<VIEW_MODEL> {
val type = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0] // index of 0 means first argument of BaseActivity class param
return type as Class<VIEW_MODEL>
}
}

Kotlin Generic abstract function

I have an abstract function, where I want to get any of my Activity Presenters in BaseActivity
protected abstract fun <T : BasePresenter<V>, V : BaseView> getPresenter(): T
So my function shoud accept only class that extends BasePresenter with view that extands BaseView
But when I implimment this function, I get an error
private lateinit var presenter: LauncherPresenter
override fun <T : BasePresenter<V>, V : BaseView> getPresenter(): T = presenter
Type mismatch.
Required: T
Found: LauncherPresenter
I know this is stupid question, but I cant get where I am wrong.
You don't need to declare generic in your function, because it already specified in your BaseActivity at the top.
Example your BaseActivity:
abstract class BaseActivity<T : BasePresenter<V>, V : BaseView>() :
AppCompatActivity(), BaseView {
protected abstract fun getPresenter(): T
}
Where T specified at the top.
When you implement function in child of BaseActivity:
class LauncherActivity() :
BaseActivity<LauncherPresenter<LauncherView>, LauncherView>(), LauncherView {
private lateinit var presenter: LauncherPresenter
//your override method
protected override fun getPresenter(): LauncherPresenter = presenter
}
You can use your code but with Unchecked cast like:
override fun <T : BasePresenter<V>, V : BaseView> getPresenter(): T = presenter as T
LauncherPresenter can't become T itself
in the form that you use

Getting generic class from Kotlin Generic

I have the following class signature:
abstract class BaseActivity<E : ViewModel> : AppCompatActivity() {
protected lateinit var viewModel: E
}
Now I want to initialize my viewModel in a generic way using ViewModelProvider, so:
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(MyViewModel::class)
Given that MyViewModel class will be provided in the generic type, I'd say this could potentially be abstracted into the BaseActivity so I dont have to do it for every Activity that extends it.
I tried with:
inline fun <reified E : ViewModel> getViewModelClass() = E::class.java
But then when doing:
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(getViewModelClass())
I get Cannot use E as reified type paramter. Use class instead
Is there a solution to this?
E in BaseActivity can't be reified, so you can't pass it to any methods which take a reified E.
Your best option may just be to accept the class as a constructor parameter.
abstract class BaseActivity<E : ViewModel>(private val modelClass: Class<E>) : AppCompatActivity() {
protected lateinit var viewModel: E
... viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(modelClass)
}
If BaseActivity wasn't abstract, you could add a factory method:
// outside BaseActivity class itself
fun <reified E : BaseModel> BaseActivity() = BaseActivity(E::class.java)
but it wouldn't help when extending it.
You ca do it in this way:
abstract class BaseActivity<E : ViewModel> : AppCompatActivity() {
protected lateinit var viewModel: E
abstract fun getViewModel():E
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(getViewModel())
}
}
Now, you can extend any class from BaseActivity and override the getViewModel() function returning the respective ViewModel class.
Hope this helps.
EDIT
Try this once:
inline fun <reified E> getViewModelClass(): Class<E> {
return E::class.java
}
and use it like this:
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(getViewModelClass())

Resources