It works fine until PagingDataEpoxyController but when passing data to EpoxyModel the apk crashes. This is my code, plaese help me
#EpoxyModelClass
abstract class PostCommentModel : EpoxyModel() {
#EpoxyAttribute
lateinit var commentData : CommentData
override fun getDefaultLayout() : Int {
return R.layout.layout_comment_item
}
override fun bind(view: LayoutCommentBinding) {
super.bind(view)
view.profileName.text = commentData.username
...
}
}
class PostCommentController( private val context: Context ) : PagingDataEpoxyController<CommentData>() {
override fun buildItemModel(currentPosition: Int, item: CommentData?): EpoxyModel<*> {
return PostCommentModel()_
.id(item!!.id)
.commentData(item)
}
}
How to make Epoxymodel usable with PagingDataEpoxyController?, Thank you...
You should add your crash logs as well so that we can know for sure where the issue lies.
By the look of the code you provided, I am almost certain that you're having a NullPointerException so I will answer accordingly.
If you read the javadoc for PagingDataEpoxyController#buildItemModel, it mentions that it passes in null values as a way of telling you that you should supply your UI with some placeholders (in order to hint the users that more items are being loaded at the moment).
/**
* Builds the model for a given item. This must return a single model for each item. If you want
* to inject headers etc, you can override [addModels] function.
*
* If the `item` is `null`, you should provide the placeholder. If your [PagedList] is
* configured without placeholders, you don't need to handle the `null` case.
*/
abstract fun buildItemModel(currentPosition: Int, item: T?): EpoxyModel<*>
You're forcing a nullable value to be force-unwrapped even though Epoxy tells you that it may be null sometimes. Hence your app crashes with NullPointerException. You should never do a force-unwrap !! if you're not 100% sure that the value can not be null.
So instead of doing:
override fun buildItemModel(currentPosition: Int, item: CommentData?): EpoxyModel<*> {
return PostCommentModel()_
.id(item!!.id) // You are force unwrapping a nullable value here
.commentData(item)
}
you should be doing something like:
override fun buildItemModel(currentPosition: Int, item: CommentData?): EpoxyModel<*> {
return if (item != null) {
PostCommentModel()_
.id(item.id) // No need force-unwrapping now! We are safe from a NullPointerException
.commentData(item)
} else {
PlaceholderModel()_
.id("placeholder-$currentPosition")
}
}
Here we checked whether or not the item is null, and showed a Placeholder when it is.
I hope this helps you!
Related
I have enum class and I am mapping by value, when I am return Enum value it always complain about null issue.
ConversationStatus.kt
enum class ConversationStatus(val status: String) {
OPEN("open"),
CLOSED("closed");
companion object {
private val mapByStatus = values().associateBy(ConversationStatus::status)
fun fromType(status: String): ConversationStatus {
return mapByStatus[status]
}
}
}
This always complain this issue. How can I fix this? Any recommendation for that. Thanks
There's 3 possible ways to go to.
Android Studio is often good at suggested fixes as you can see in the screenshot. It suggests to change the return type to ConversationStatus? which means it might return null. It will become this then:
companion object {
private val mapByStatus = values().associateBy(ConversationStatus::status)
fun fromType(status: String): ConversationStatus? {
return mapByStatus[status]
}
}
Another way is to tell the compiler that you ensure it will always not be null by adding !! to the return statement. Like this:
companion object {
private val mapByStatus = values().associateBy(ConversationStatus::status)
fun fromType(status: String): ConversationStatus {
return mapByStatus[status]!!
}
}
This will cause a crash though if you call the function with a status that's not "open" or "closed"
Alternatively you could provide a fall back value. With this I mean that it returns a default value in case you call the function with a string that's not "open" or "closed". If you want that to be OPEN you could do like this:
companion object {
private val mapByStatus = values().associateBy(ConversationStatus::status)
fun fromType(status: String): ConversationStatus {
return mapByStatus[status] ?: OPEN
}
}
Background
I need to use a translations-SDK (Lokalise, docs here) that is intended to load strings resources from their servers.
This means that if you use getString , it will prefer what's on the server instead of what's on the app. This includes also the cases of inflation of layout XML files.
The problem
It seems that Android doesn't have a global resource handling that I can use. This is why the SDK says I should use one of these :
For Activity, I can override the callback of attachBaseContext.
For all other cases, that I need to get the resources of them, I can use LokaliseResources(context) .
Thing is, a lot of code in the app I work on doesn't involve an Activity. A lot of the UI on the app is floating (using SAW permission, AKA "System Alert Window").
This means that there is a lot of inflation of Views using just the Application class.
What I've tried
First I made a simple manager for this:
object TranslationsManager {
var resources: LokaliseResources? = null
#UiThread
fun initOnAppOnCreate(context: App) {
Lokalise.init(context, Keys.LOCALISE_SDK_TOKEN, Keys.LOCALISE_PROJECT_ID)
Lokalise.updateTranslations()
resources = LokaliseResources(context)
}
fun getResources(context: Context): Resources {
return resources ?: context.resources
}
}
I tried to perform various things using the library, but they crashed as it's not how the library works.
So these failed:
For the getResources of the class that extends Application, I tried to return the one of the SDK
Use attachBaseContext of the class that implements Application. This causes a crash since it needs to be initialized before, so I tried to initialize it right in this callback, but still got a crash.
For LayoutInflater, I tried to use LayoutInflater.from(new ContextThemeWrapper(...)) , and override its getResources callback, but it didn't do anything.
I tried to use Philology library by having this:
object MyPhilologyRepositoryFactory : PhilologyRepositoryFactory {
override fun getPhilologyRepository(locale: Locale): PhilologyRepository {
return object : PhilologyRepository {
override fun getPlural(key: String, quantityString: String): CharSequence? {
Log.d("AppLog", "getPlural $key")
return TranslationsManager.resources?.getString(quantityString)
?: super.getPlural(key, quantityString)
}
override fun getText(key: String): CharSequence? {
Log.d("AppLog", "getText $key")
return TranslationsManager.resources?.getString(key) ?: super.getText(key)
}
override fun getTextArray(key: String): Array<CharSequence>? {
Log.d("AppLog", "getTextArray $key")
TranslationsManager.resources?.getStringArray(key)?.let { stringArray ->
val result = Array<CharSequence>(stringArray.size) { index ->
stringArray[index]
}
return result
}
return super.getTextArray(key)
}
}
}
}
And on the class that extends Application, use this:
Philology.init(MyPhilologyRepositoryFactory)
ViewPump.init(ViewPump.builder().addInterceptor(PhilologyInterceptor).build())
But when inflation was used in the app (and actually everywhere), I never saw that this code is being used, ever.
That being said, this is what I've succeeded:
1.For all Activities/Services, indeed I've added usage of attachBaseContext as the SDK says:
override fun attachBaseContext(newBase: Context) {
super.attachBaseContext(LokaliseContextWrapper.wrap(newBase))
}
2.For all custom views, I've used what I've made:
override fun getResources(): Resources {
return TranslationsManager.getResources(context)
}
Both of these took quite some time to find and add manually, one after another.
Sadly, still there seem to be some important cases.
I've found that at least for layout inflation (in the custom views, for example), the layout XML files don't take the resources from the SDK.
I've found an article "Taming Android Resources and LayoutInflater for string manipulation" from 2020 (cache here) saying I could use some trick of ContextThemeWrapper a bit more complex than what I tried, but sadly it lacks some important information (implementation of cloneInContext for example) that I've failed to use:
class CustomContextWrapper(
private val base: Context,
private val dynamicStringMap: Map<String, String>
) : ContextWrapper(base) {
override fun getResources() = CustomResources(base.resources, dynamicStringMap)
override fun getSystemService(name: String): Any? {
if (Context.LAYOUT_INFLATER_SERVICE == name) {
return CustomLayoutInflater(LayoutInflater.from(baseContext), this)
}
return super.getSystemService(name)
}
}
class CustomLayoutInflater constructor(
original: LayoutInflater,
newContext: Context,
) : LayoutInflater(original, newContext) {
override fun cloneInContext(p0: Context?): LayoutInflater {
TODO("Not yet implemented")
}
override fun onCreateView(name: String, attrs: AttributeSet): View? {
try {
val view = createView(name, "android.widget.", attrs)
if (view is TextView) {
// Here we get original TextView and then return it after overriding text
return overrideTextView(view, attrs)
}
} catch (e: ClassNotFoundException) {
} catch (inflateException: InflateException) {
}
return super.onCreateView(name, attrs)
}
private fun overrideTextView(view: TextView, attrs: AttributeSet?): TextView {
val typedArray =
view.context.obtainStyledAttributes(attrs, intArrayOf(android.R.attr.text))
val stringResource = typedArray.getResourceId(0, -1)
view.text = view.resources.getText(stringResource)
typedArray.recycle()
return view
}
}
However, it said I could use a library called "ViewPump" (here, and it actually suggested to use Philology library here) that will do the trick for me, and that from Android 30 we could use ResourcesProvider and ResourcesLoader classes. Sadly I couldn't find an example to use any of these for the purpose I'm working on.
The questions
Is it really possible to use the trick that was mentioned on the article? What should be done to use it properly?
How can I use the "ViewPump"/"Philology" library to achieve the same thing?
Is there any way to offer resources globally instead of using all that I've mentioned? So that all resources will be using the translation SDK, no matter where and how I reach the resources ? This takes a lot of time already, as I need to go over many classes and add handling of resources myself...
Will any of the above cover all cases? For example not just the inflation, but other cases such as TextView.setText(resId) ?
As for the new classes of Android API 30, because they are very new, I've decided to ask about them in a new post, here.
EDIT: Talking with Lokalise support, they said they already do use ViewPump, which means that it probably works in cases that don't match what I have.
I've found success with a combination of using ViewPump to wrap the context of the view being inflated with your ContextWrapper.
class ContextWrappingViewInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): InflateResult {
val request = chain.request()
val newRequest = request.toBuilder()
.context(MyContextWrapper.wrap(request.context))
.build()
return chain.proceed(newRequest)
}
}
However I haven't found a solution to force custom view attributes to use your context for free. The issue is that internally, styled attributes fetch their resources from what has already been cached internally via XML files. Meaning, the view's context doesn't come into it at all.
A workaround for this is to fetch the resource ID from styled attributes and then delegate the actual resource fetching to context.
fun TypedArray.getStringUsingContext(context: Context, index: Int): String? {
if (hasValue(index)) {
return getResourceId(index, 0)
.takeIf { it != 0 }
?.let { context.getString(it) }
}
return null
}
Usage in CustomView:
init {
context.obtainStyledAttributes(attrs, R.styleable.CustomView).use { array ->
val myText = array.getStringUsingContext(context, R.styleable.CustomView_myText)
...
}
}
I do not have very much experience with Kotlin, been doing most of my development in Java. I have converted my ArtistFragment to Kotlin in order to solve another issue. But after converting it I am getting to errors that I do not know how to resolve.
The first one is Smart cast to 'RecyclerView!' is impossible, because 'recyclerView' is a mutable property that could have been changed by this time and the second one is Cannot create an instance of an abstract class
I searched through Stackoverflow, but since I don't really understand the what the problem is I am not sure if they are relevant to my problems.
Here is my converted ArtistFragment:
class ArtistFragment : Fragment() {
var spanCount = 3 // 2 columns
var spacing = 20 // 20px
var includeEdge = true
private var recyclerView: RecyclerView? = null
private var adapter: ArtistAdapter? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_artist, container, false)
recyclerView = view.findViewById(R.id.artistFragment)
recyclerView.setLayoutManager(GridLayoutManager(activity, 3))
LoadData().execute("")
return view
}
abstract inner class LoadData : AsyncTask<String?, Void?, String>() {
protected fun doInBackground(vararg strings: String): String {
if (activity != null) {
adapter = ArtistAdapter(activity, ArtistLoader().artistList(activity))
}
return "Executed"
}
override fun onPostExecute(s: String) {
recyclerView!!.adapter = adapter
if (activity != null) {
recyclerView!!.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, includeEdge))
}
}
override fun onPreExecute() {
super.onPreExecute()
}
}
}
I am seeing Smart cast to 'RecyclerView!' is impossible, because 'recyclerView' is a mutable property that could have been changed by this time here in this section: recyclerView.setLayoutManager(GridLayoutManager(activity, 3))
I am seeing Cannot create an instance of an abstract class here in this section LoadData().execute("")
I hoping someone can explain these error and how to fix them.
Thanks
As your recyclerview is a nullable property, You need to modify your code as below -
recyclerView?.setLayoutManager(GridLayoutManager(activity, 3))
Remove abstract from class
inner class LoadData : AsyncTask<String?, Void?, String>()
Smart cast to 'RecyclerView!' is impossible, because 'recyclerView' is a mutable property that could have been changed by this time is because recyclerView is a var. Kotlin has the concept of mutable (var) and immutable (val) variables, where a val is a fixed value, but a var can be reassigned.
So the issue is that recyclerView is a nullable type (RecyclerView?) and it could be null - so you need to make sure it's not null before you access it, right? findViewById returns a non-null RecyclerView, which gets assigned to recyclerView, so the compiler knows it can't be null.
But then it gets to the next line - what if the value of recyclerView has changed since the previous line? What if something has modified it, like on another thread? It's a var so that's possible, and the compiler can't make any guarantees about whether it's null or not anymore. So it can't "smart cast to RecyclerView!" (! denotes non-null) and let you just treat it as a non-null type in your code. You have to null check it again.
What Priyanka's code (recyclerView?.setLayoutManager(GridLayoutManager(activity, 3))) does is null-checks recyclerView, and only makes the call if it's non-null. Under the hood, it's copying recyclerView to a temporary val (which isn't going to change) and checks that, and then executes the call on that.
This is a pretty standard thing with Kotlin, and not just for nullable types - any var could possibly change at any moment, so it's typical to assign it to a temporary variable so you can do the stuff on that stable instance. Kotlin has a bunch of scope functions which are convenient for doing things on an object, and one of the benefits is that you know the reference you're acting on isn't going to change halfway through
I'm using ViewPager2, Kotlin and AndroidX.
When the adapter is not at index 0 and I change the adapter list and set current item to index 0 the exception is thrown.
java.lang.IllegalStateException: Design assumption violated.
at androidx.viewpager2.widget.ViewPager2.updateCurrentItem(ViewPager2.java:538)
at androidx.viewpager2.widget.ViewPager2$4.onAnimationsFinished(ViewPager2.java:518)
at androidx.recyclerview.widget.RecyclerView$ItemAnimator.isRunning(RecyclerView.java:13244)
at androidx.viewpager2.widget.ViewPager2.onLayout(ViewPager2.java:515)
at android.view.View.layout(View.java:15596)
In line 537 of ViewPager2 there is an if which causes the exception:
// Extra checks verifying assumptions
// TODO: remove after testing
View snapView1 = mPagerSnapHelper.findSnapView(mLayoutManager);
View snapView2 = mLayoutManager.findViewByPosition(snapPosition);
if (snapView1 != snapView2) {
throw new IllegalStateException("Design assumption violated.");
}
This is how I'm updating the adapter list:
adapter.list = newList
adapter.notifyDataSetChanged()
viewpager.setCurrentItem(0, true)
It happens only when smoothScroll is set to true
What am I missing?
Edit :
I'm using androidx.viewpager2.widget.ViewPager2 available in com.google.android.material:material:1.1.0-alpha08
I had this same problem with ViewPager2 on configuration change. I'm using:
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.viewpager2:viewpager2:1.0.0-beta03'
In my case, I had overridden the getItemId() method in my FragmentStateAdapter class. When I got rid of my getItemId() method, the "IllegalStateException: Design assumption violated" error was no more! :-)
I'm using androidx.viewpager2:viewpager2:1.0.0
For whoever still stuck at this, and need to implement getItemId() containsItem(). Please chek your getItemId() and containsItem() implementation. My problem is when I serve 10 item with possibility several item have same value, this error occurs.
Media have several data with same value.
private var mediaId = media.map { it.hashCode().toLong() }
override fun getItemId(position: Int): Long = mediaId[position]
override fun containsItem(itemId: Long): Boolean = mediaId.contains(itemId)
while you updating the data or even simply swiping, the viewpager is confuse because you implement getItemId() with non unique id. The solution is just make sure your data is unique or have their own id. *My media datas dont have id.
If you open the implementation of getItemId() notes this part :
#return stable item id {#link RecyclerView.Adapter#hasStableIds()}
you need to have unique id for each of item.
I had the same problem but the bug seems to be fixed (at least for me) in the current version androidx.viewpager2:viewpager2:1.0.0-beta01.
See more in ViewPager2 1.0.0-beta01 Release Notes
Here is the solution for a straightforward use case
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
class ViewPager2Adapter(
val dataList: List<Item> = listOf(),
fragmentActivity: FragmentActivity
) :
FragmentStateAdapter(fragmentActivity) {
val pageIds= dataList.map { it.hashCode().toLong() }.toMutableList()
override fun createFragment(position: Int): Fragment {
return MyFragment.newInstance(dataList[position])
}
override fun getItemCount(): Int {
return dataList.size
}
override fun getItemId(position: Int): Long {
return pageIds[position]
}
override fun containsItem(itemId: Long): Boolean {
return pageIds.contains(itemId)
}
}
Then I encountered a specific use case when I was replacing my object at the position with a new one then I got the exception.
Solution:
Now in case you are making data changes to the adapter i.e. with replacing an object in dataList with a new one, then make sure to replace id in pageIds of the old object with the new one.
(viewPager2.adapter as? ViewPager2Adapter)?.apply {
pageIds[oldIndex] = (NewItemObject).hashCode().toLong()
notifyItemChanged(oldIndex)
}
My issue was that I was customizing containsItem() to have some conditions under which I remove Fragments from the ViewPager, and then return false;
I encounter Design assumption violated after rotating the phone (and then swipe the ViewPager2).
I solved that by just returning the super class method: return super.containsItem(itemId);
#Override
public boolean containsItem(long itemId) {
// Your code
return super.containsItem(itemId);
}
UPDATE:
The containsItem method does not have the "CallSuper" annotation and is therefore not required to be called
This is right, no need to call the super; and I didn't call it explicitly before returning a boolean; just returning it solving this issue
The fun findLast will return MDetail? , so the value of var aa maybe be null.
The fun remove accept a non-null parameter, but why can the code listofMDetail.remove(aa) be compiled ? Thanks!
And more, the Code A can run normally!
Code A
private val listofMDetail:MutableList<MDetail> = myGson.fromJson<MutableList<MDetail>>(mJson)
fun deleteDetailByID(_id:Long){
var aa=listofMDetail.findLast { it._id == _id };
//listofMDetail.remove(null) //It doesn't work
listofMDetail.remove(aa) // It can be compiled
var bb: MDetail?=null
listofMDetail.remove(bb) // It can be compiled
}
Source Code
public interface MutableList<E> : List<E>, MutableCollection<E> {
// Modification Operations
override fun add(element: E): Boolean
override fun remove(element: E): Boolean
...........
}
In your code, aa and bb both are of type MDetail?, but the null value itself contains no info about type, so the compiler cannot infer the type for you and it's a compile error, but if you cast the null to MDetail?, then it will be compiled as well:
listofMDetail.remove(null as MDetail?)
Then the problem is, why the remove works when your listofMDetail is declared as MutableList<MDetail> with no ? after MDetail.
That's because the remove method is not resolved to public interface MutableList<E>, but MutableCollections.kt's remove, here is the code:
package kotlin.collections
/**
* Removes a single instance of the specified element from this
* collection, if it is present.
*
* Allows to overcome type-safety restriction of `remove` that requires to pass an element of type `E`.
*
* #return `true` if the element has been successfully removed; `false` if it was not present in the collection.
*/
#kotlin.internal.InlineOnly
public inline fun <#kotlin.internal.OnlyInputTypes T> MutableCollection<out T>.remove(element: T): Boolean
= #Suppress("UNCHECKED_CAST") (this as MutableCollection<T>).remove(element)
In your case, the generic type T is MDetail?, and MDetail is out T, so the remove will receive a parameter of type MDetail?, which permits null value.