Could anyone help me to tell how can I disable the firebase logging in Google ML Kit library for android. For every 15 mins it will POST some information to https://firebaselogging.googleapis.com/v0cc/log/batch?format=json_proto3
I tried using the recommendation from google https://firebase.google.com/docs/perf-mon/disable-sdk?platform=android#kotlin+ktx and I am also not sure whether it is a right way.
Suggestions are welcome.
Google's guide from the question didn't work for me, so I have looked for alternatives.
The library is obfuscated, so it is hard to be sure, but it appears that the logging is hardcoded in. However, there is a very hacky way to disable it through some fragile reflection:
import android.util.Log
import com.google.mlkit.common.sdkinternal.LazyInstanceMap
import java.lang.reflect.Field
/**
* This class tries to disable MLKit's phoning home/logging.
* This is extremely hacky and will probably break in the next update (obfuscated class names will probably need renaming).
*
* This class exploits the fact, that there are multiple options classes which control this
* (look for "MLKitLoggingOptions" in toString implementation) and for some reason MLKit uses them as keys
* in LazyInstanceMaps which exist as static (usually) variables (which are themselves lazy).
*
* This makes sure that the LazyInstanceMaps exist, then it hijacks their internal HashMap implementation
* and replaces it with a custom map, that creates instances of whatever with logging disabled.
*
* The way to detect which holder classes need renaming, look at the stack trace, for example:
* ```
java.lang.NoClassDefFoundError: Failed resolution of: Lcom/google/android/datatransport/cct/CCTDestination;
at com.google.android.gms.internal.mlkit_vision_barcode.zznu.<init>(com.google.android.gms:play-services-mlkit-barcode-scanning##18.0.0:1)
at com.google.android.gms.internal.mlkit_vision_barcode.zznf.<init>(com.google.android.gms:play-services-mlkit-barcode-scanning##18.0.0:3)
at com.google.android.gms.internal.mlkit_vision_barcode.zznw.create(com.google.android.gms:play-services-mlkit-barcode-scanning##18.0.0:4)
at com.google.mlkit.common.sdkinternal.LazyInstanceMap.get(com.google.mlkit:common##18.0.0:3)
at com.google.android.gms.internal.mlkit_vision_barcode.zznx.zza(com.google.android.gms:play-services-mlkit-barcode-scanning##18.0.0:2)
at com.google.android.gms.internal.mlkit_vision_barcode.zznx.zzb(com.google.android.gms:play-services-mlkit-barcode-scanning##18.0.0:3)
at com.google.mlkit.vision.barcode.internal.zzf.create(com.google.android.gms:play-services-mlkit-barcode-scanning##18.0.0:3)
at com.google.mlkit.common.sdkinternal.LazyInstanceMap.get(com.google.mlkit:common##18.0.0:3)
at com.google.mlkit.vision.barcode.internal.zze.zzb(com.google.android.gms:play-services-mlkit-barcode-scanning##18.0.0:2)
at com.google.mlkit.vision.barcode.BarcodeScanning.getClient(com.google.android.gms:play-services-mlkit-barcode-scanning##18.0.0:3)
* ```
* here are two LazyInstanceMap lookups, of which only the second one (through trial and error or with debugger)
* uses MLKitLoggingOptions keys. From here we can find that the holder class is com.google.android.gms.internal.mlkit_vision_barcode.zznx .
*/
object MLKitTrickery {
private class mlkit_vision_barcodeLoggingOptions(base: com.google.android.gms.internal.mlkit_vision_barcode.zzne) : com.google.android.gms.internal.mlkit_vision_barcode.zzne() {
private val libraryName: String = base.zzb()
private val firelogEventType: Int = base.zza()
override fun zza(): Int = firelogEventType
override fun zzb(): String = libraryName
override fun zzc(): Boolean = false //enableFirelog
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as mlkit_vision_barcodeLoggingOptions
if (libraryName != other.libraryName) return false
if (firelogEventType != other.firelogEventType) return false
return true
}
override fun hashCode(): Int {
var result = libraryName.hashCode()
result = 31 * result + firelogEventType
return result
}
}
private class mlkit_vision_commonLoggingOptions(base: com.google.android.gms.internal.mlkit_vision_common.zzjn) : com.google.android.gms.internal.mlkit_vision_common.zzjn() {
private val libraryName: String = base.zzb()
private val firelogEventType: Int = base.zza()
override fun zza(): Int = firelogEventType
override fun zzb(): String = libraryName
override fun zzc(): Boolean = false //enableFirelog
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as mlkit_vision_commonLoggingOptions
if (libraryName != other.libraryName) return false
if (firelogEventType != other.firelogEventType) return false
return true
}
override fun hashCode(): Int {
var result = libraryName.hashCode()
result = 31 * result + firelogEventType
return result
}
}
private fun isMLKitLoggingOptions(obj: Any): Boolean {
return obj is com.google.android.gms.internal.mlkit_vision_barcode.zzne
|| obj is com.google.android.gms.internal.mlkit_vision_common.zzjn
}
private fun convertMLKitLoggingOptions(obj: Any): Any? {
if (obj is com.google.android.gms.internal.mlkit_vision_barcode.zzne) {
return mlkit_vision_barcodeLoggingOptions(obj)
}
if (obj is com.google.android.gms.internal.mlkit_vision_common.zzjn) {
return mlkit_vision_commonLoggingOptions(obj)
}
return null
}
#Suppress("UNCHECKED_CAST")
private fun patchLazyMap(lazyMapHolder:Any?, lazyMapHolderClass: Class<*>) {
val holderField = lazyMapHolderClass.declaredFields.find { LazyInstanceMap::class.java.isAssignableFrom(it.type) }!!
var currentLazyInstanceMap = holderField.get(lazyMapHolder)
if (currentLazyInstanceMap == null) {
var lastError: Throwable? = null
for (constructor in holderField.type.declaredConstructors) {
try {
constructor.isAccessible = true
val params = arrayOfNulls<Any?>(constructor.parameterCount)
currentLazyInstanceMap = constructor.newInstance(*params)
holderField.set(lazyMapHolder, currentLazyInstanceMap)
} catch (e:Throwable) {
lastError = e
}
}
if (currentLazyInstanceMap == null) {
throw java.lang.Exception("Failed to initialize LazyInstanceMap "+holderField.type, lastError)
}
}
var mapHolderClass: Class<*> = currentLazyInstanceMap.javaClass
val createMethod = mapHolderClass.getDeclaredMethod("create", Object::class.java)
val mapField: Field
while (true) {
val mapFieldCandidate = mapHolderClass.declaredFields.firstOrNull { Map::class.java.isAssignableFrom(it.type) }
if (mapFieldCandidate != null) {
mapField = mapFieldCandidate
break
}
mapHolderClass = mapHolderClass.superclass ?: error("It appears that ${currentLazyInstanceMap.javaClass} does not have a backing map field")
}
val oldMap = mapField.get(currentLazyInstanceMap) as MutableMap<Any, Any?>
val customMap = object : MutableMap<Any, Any?> by oldMap {
override fun containsKey(key: Any): Boolean {
if (oldMap.containsKey(key)) {
return true
}
if (isMLKitLoggingOptions(key)) {
return true
}
return false
}
override fun get(key: Any): Any? {
val existing = oldMap.get(key)
if (existing != null) {
return existing
}
val convertedKey = convertMLKitLoggingOptions(key)
if (convertedKey != null) {
val created = createMethod.invoke(currentLazyInstanceMap, convertedKey)
oldMap.put(key, created)
return created
}
return null
}
}
mapField.isAccessible = true
mapField.set(currentLazyInstanceMap, customMap)
}
private var initialized = false
/**
* Call this to attempt to disable MLKit logging.
*/
fun init() {
try {
patchLazyMap(null, com.google.android.gms.internal.mlkit_vision_barcode.zznx::class.java)
patchLazyMap(null, com.google.android.gms.internal.mlkit_vision_common.zzkc::class.java)
initialized = true
} catch (e: Throwable) {
Log.e("MLKitTrickery", "Failed to disable MLKit phoning home")
}
}
}
When you also shim out GMS TelemetryLogging with:
#file:Suppress("unused", "UNUSED_PARAMETER")
package com.google.android.gms.common.internal
import android.app.Activity
import android.content.Context
import android.os.Parcel
import com.google.android.gms.tasks.OnFailureListener
import com.google.android.gms.tasks.OnSuccessListener
import com.google.android.gms.tasks.Task
import java.util.concurrent.Executor
class TelemetryLoggingOptions {
class Builder {
fun setApi(api: String?): Builder = this
fun build(): TelemetryLoggingOptions = TelemetryLoggingOptions()
}
companion object {
#JvmStatic
fun builder(): Builder = Builder()
}
}
private object DummyLogTask : Task<Void?>() {
override fun addOnFailureListener(p0: OnFailureListener): Task<Void?> {
// Implemented, because failing tells MLKit to back-off for 30 minutes, which is a win for performance
p0.onFailure(exception)
return this
}
override fun addOnFailureListener(p0: Activity, p1: OnFailureListener): Task<Void?> = addOnFailureListener(p1)
override fun addOnFailureListener(p0: Executor, p1: OnFailureListener): Task<Void?> = addOnFailureListener(p1)
override fun addOnSuccessListener(p0: OnSuccessListener<in Void?>): Task<Void?> = this
override fun addOnSuccessListener(p0: Activity, p1: OnSuccessListener<in Void?>): Task<Void?> = addOnSuccessListener(p1)
override fun addOnSuccessListener(p0: Executor, p1: OnSuccessListener<in Void?>): Task<Void?> = addOnSuccessListener(p1)
override fun getException(): Exception? = exception
override fun getResult(): Void? = null
override fun <X : Throwable?> getResult(p0: Class<X>): Void? = null
override fun isCanceled(): Boolean = false
override fun isComplete(): Boolean = true
override fun isSuccessful(): Boolean = false
private val exception = Exception("Success was never an option")
}
object TelemetryLogging {
#JvmStatic
fun getClient(context: Context): TelemetryLoggingClient {
return object : TelemetryLoggingClient {
override fun log(data: TelemetryData): Task<Void?> {
return DummyLogTask
}
}
}
#JvmStatic
fun getClient(context: Context, options: TelemetryLoggingOptions): TelemetryLoggingClient {
return getClient(context)
}
}
interface TelemetryLoggingClient {
fun log(data: TelemetryData): Task<Void?>
}
class TelemetryData(var1: Int, var2:List<MethodInvocation>?) {
fun writeToParcel(var1: Parcel, var2: Int) {}
}
class MethodInvocation {
constructor(methodKey:Int, resultStatusCode:Int, connectionResultStatusCode:Int,
startTimeMillis:Long, endTimeMillis:Long,
callingModuleId: String?, callingEntryPoint: String?, serviceId:Int)
constructor(methodKey:Int, resultStatusCode:Int, connectionResultStatusCode:Int,
startTimeMillis:Long, endTimeMillis:Long,
callingModuleId: String?, callingEntryPoint: String?,
serviceId:Int, var11:Int)
fun writeToParcel(var1: Parcel, var2: Int) {}
}
it is possible to trim many transitive dependencies and save apk size:
implementation("com.google.mlkit:barcode-scanning:17.0.2") {
exclude("com.google.android.gms", "play-services-base")
exclude("com.google.android.datatransport", "transport-api")
exclude("com.google.android.datatransport", "transport-backend-cct")
exclude("com.google.android.datatransport", "transport-runtime")
exclude("com.google.firebase", "firebase-encoders-json")
exclude("com.google.firebase", "firebase-encoders")
}
However, as noted above, this is very fragile and will probably somehow break after MLKit update. It would be nice if this was not needed.
Related
I am using MutableSharedFlow in project. My main project concept is very big, so I cannot add in here, instead I made a very small sample to reproduce my problem. I know this example is very wrong, but I have same scenario in my main project. I am using MutableSharedFlow as a Queue implementation with single Thread execution with the help of Mutex.
ExampleViewModel
class ExampleViewModel : ViewModel() {
val serviceNumber = ServiceNumber()
val serviceNumberEventFlow = serviceNumber.eventFlow
val mutex = Mutex()
var delayCounter = 0
suspend fun addItem(itemOne: Int = 2, itemTwo: Int = 2): Add {
return mutex.queueWithTimeout("add") {
serviceNumberEventFlow.onSubscription {
serviceNumber.add(itemOne, itemTwo)
delayCounter++
if (delayCounter == 1) {
delay(1000)
Log.w("Delay ", "Delay Started")
serviceNumber.add(8, 8)
}
}.firstOrNull {
it is Add
} as Add? ?: Add("No value")
}
}
suspend fun subItem(itemOne: Int = 2, itemTwo: Int = 2): Sub {
return mutex.queueWithTimeout("sub") {
serviceNumberEventFlow.onSubscription {
serviceNumber.sub(itemOne, itemTwo)
}.firstOrNull {
it is Sub
} as Sub? ?: Sub("No value")
}
}
private suspend fun <T> Mutex.queueWithTimeout(
action: String, timeout: Long = 5000L, block: suspend CoroutineScope.() -> T
): T {
return try {
withLock {
return#withLock withTimeout<T>(timeMillis = timeout, block = block)
}
} catch (e: Exception) {
Log.e("Wrong", " $e Timeout on BLE call: $action")
throw e
}
}
}
class ServiceNumber : Number {
val eventFlow = MutableSharedFlow<Event>(extraBufferCapacity = 50)
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun add(itemOne: Int, itemTwo: Int) {
Log.i("ServiceNumber", " Add event trigger with $itemOne -- $itemTwo")
eventFlow.emitEvent(Add("Item added ${itemOne + itemTwo}"))
}
override fun sub(itemOne: Int, itemTwo: Int) {
eventFlow.emitEvent(Sub("Item subtract ${itemOne - itemTwo}"))
}
private fun <T> MutableSharedFlow<T>.emitEvent(event: T) {
scope.launch { emit(event) }
}
}
interface Number {
fun add(itemOne: Int, itemTwo: Int)
fun sub(itemOne: Int, itemTwo: Int)
}
sealed class Event
data class Add(val item: String) : Event()
data class Sub(val item: String) : Event()
MainActivity.kt
class MainActivity : AppCompatActivity() {
private val viewModel: ExampleViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Theme {
Column {
Button(onClick = {
lifecycleScope.launchWhenCreated {
withContext(Dispatchers.IO) {
val result = viewModel.addItem()
Log.e("Result", "$result")
}
}
}) {
Text("Add")
}
Button(onClick = {
lifecycleScope.launchWhenCreated {
withContext(Dispatchers.IO) {
val result = viewModel.subItem()
Log.e("Result", "$result")
}
}
}) {
Text("Sub")
}
}
}
}
}
}
#Composable
fun Theme(content: #Composable () -> Unit) {
MaterialTheme(content = content)
}
Problem
This example is simple Add and subtract of two number. When I am click on Add Button first time, viewmodel.addItem(...) -> ... ->ServiceNumber.add() will trigger and emit the value and we can see log in console. Inside the Add Button function, I was also added a delay to trigger ServiceNumber.add() again to see that onSubscription will be also retrigger or not. MutableSharedFlow emit the value as I can see in log but onSubscription method not called. I don't understand what is the problem in here.
onSubscription is an operator so it creates a new copy of your shared flow. The lambda code will only be run when there are new collectors on this new flow. The only time you collect this new flow is when you call firstOrNull() on it, a terminal operator that collects a single value.
I would like to use Mapbox to get the road speed limit value but returned the value from LocationObserver always is null while speed limit value in the official map box app has value In the same direction. how can i fix this problem?
setup mapBoxNavigation:
if (!MapboxNavigationApp.isSetup()) {
MapboxNavigationApp.setup {
NavigationOptions.Builder(this)
.accessToken(MAPBOX_ACCESS_TOKEN)
.build()
}
}
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
MapboxNavigationApp.attach(owner)
}
override fun onPause(owner: LifecycleOwner) {
MapboxNavigationApp.detach(owner)
}
})
MapboxNavigationApp.current()?.startTripSession()
This is observer dataSource:
class MapBoxLocationObserverDataSource #Inject constructor(context: Context) :
MapboxNavigationObserver, MapBoxLocationObserver {
private val speedLimitCallback = MutableStateFlow<NavigationInfo?>(null)
override val speedLimit: Flow<NavigationInfo?>
get() = speedLimitCallback
private val locationObserver = object : LocationObserver {
override fun onNewLocationMatcherResult(locationMatcherResult: LocationMatcherResult) {
MapboxSpeedInfoApi().updatePostedAndCurrentSpeed(
locationMatcherResult,
DistanceFormatterOptions.Builder(context).build(),
).apply {
speedLimitCallback.value =
NavigationInfo(speedInfo = this, locationMatcherResult = locationMatcherResult)
}
}
override fun onNewRawLocation(rawLocation: Location) = Unit
}
override fun onAttached(mapboxNavigation: MapboxNavigation) {
mapboxNavigation.registerLocationObserver(locationObserver)
}
override fun onDetached(mapboxNavigation: MapboxNavigation) {
mapboxNavigation.unregisterLocationObserver(locationObserver)
}
data class NavigationInfo(
val speedInfo: SpeedInfoValue,
val locationMatcherResult: LocationMatcherResult
)
}
The speed limit always is null in the LocationMatcherResult but some value are not, like current speed or enhancedLocation
For an app I am making I have a list in which I display pixel art creations, I do this with a RecyclerView and DiffUtil, here is the code:
package com.therealbluepandabear.pixapencil.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.therealbluepandabear.pixapencil.R
import com.therealbluepandabear.pixapencil.databinding.RecentCreationsLayoutBinding
import com.therealbluepandabear.pixapencil.enums.SnackbarDuration
import com.therealbluepandabear.pixapencil.extensions.setOnLongPressListener
import com.therealbluepandabear.pixapencil.extensions.showSnackbar
import com.therealbluepandabear.pixapencil.listeners.RecentCreationsListener
import com.therealbluepandabear.pixapencil.models.PixelArt
import com.therealbluepandabear.pixapencil.viewholders.PixelArtViewHolder
class PixelArtAdapter(
private val snackbarView: View,
private val listener: RecentCreationsListener,
private val context: Context
) : ListAdapter<PixelArt, RecyclerView.ViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val binding = RecentCreationsLayoutBinding.inflate(LayoutInflater.from(parent.context))
return PixelArtViewHolder(binding, context)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val pixelArt = getItem(position)
if (holder is PixelArtViewHolder) {
holder.bind(pixelArt)
holder.binding.recentCreationsLayoutMaterialCardView.setOnClickListener {
listener.onCreationTapped(pixelArt)
}
holder.binding.recentCreationsLayoutMaterialCardView.setOnLongPressListener {
listener.onCreationLongTapped(pixelArt)
}
holder.binding.recentCreationsLayoutFavoriteButton.setOnClickListener {
if (pixelArt.starred) {
pixelArt.starred = false
listener.onUnstarredTapped(pixelArt)
unFavouriteRecentCreation(snackbarView, pixelArt)
holder.bind(pixelArt)
} else {
pixelArt.starred = true
listener.onStarredTapped(pixelArt)
favouriteRecentCreation(snackbarView, pixelArt)
holder.bind(pixelArt)
}
}
}
}
private fun favouriteRecentCreation(contextView: View, pixelArt: PixelArt) { // move to listener
contextView.showSnackbar(contextView.context.getString(R.string.snackbar_pixel_art_project_saved_to_starred_items_in_code_str, pixelArt.title), SnackbarDuration.Default)
pixelArt.starred = true
}
private fun unFavouriteRecentCreation(contextView: View, pixelArt: PixelArt) {
contextView.showSnackbar(contextView.context.getString(R.string.snackbar_pixel_art_project_removed_from_starred_items_in_code_str, pixelArt.title), SnackbarDuration.Default)
pixelArt.starred = false
}
companion object {
val diffCallback: DiffUtil.ItemCallback<PixelArt> = object : DiffUtil.ItemCallback<PixelArt>() {
override fun areItemsTheSame(oldItem: PixelArt, newItem: PixelArt): Boolean {
return oldItem.objId == newItem.objId
}
override fun areContentsTheSame(oldItem: PixelArt, newItem: PixelArt): Boolean {
return oldItem == newItem
}
}
}
}
ViewHolder:
class PixelArtViewHolder(val binding: RecentCreationsLayoutBinding, private val context: Context) : RecyclerView.ViewHolder(binding.root) {
private fun loadPixelArtCoverImage(pixelArt: PixelArt) {
val widthHeight = if (context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
350
} else {
750
}
val requestOptions: RequestOptions = RequestOptions()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.dontAnimate()
.priority(Priority.IMMEDIATE)
.encodeFormat(Bitmap.CompressFormat.PNG)
.override(widthHeight, widthHeight)
.centerInside()
.format(DecodeFormat.DEFAULT)
Glide.with(itemView.context)
.setDefaultRequestOptions(requestOptions)
.load(File(itemView.context.getFileStreamPath(pixelArt.coverBitmapFilePath).absolutePath))
.transition(DrawableTransitionOptions.withCrossFade())
.placeholder(R.drawable.transparent_placeholder)
.into(binding.recentCreationsLayoutImageView)
}
private fun loadPixelArtTitle(pixelArt: PixelArt) {
if (pixelArt.title.length > 6) {
binding.recentCreationsLayoutTitle.ellipsize = TextUtils.TruncateAt.MARQUEE
binding.recentCreationsLayoutTitle.isSelected = true
binding.recentCreationsLayoutTitle.isSingleLine = true
(pixelArt.title + " ".repeat(10)).repeat(200).also { binding.recentCreationsLayoutTitle.text = it }
} else {
binding.recentCreationsLayoutTitle.text = pixelArt.title
}
}
private fun loadPixelArtStarred(pixelArt: PixelArt) {
binding.recentCreationsLayoutFavoriteButton.setImageResource(
if (pixelArt.starred) {
R.drawable.ic_baseline_star_24
} else {
R.drawable.ic_baseline_star_border_24
}
)
}
fun bind(pixelArt: PixelArt){
loadPixelArtCoverImage(pixelArt)
binding.recentCreationsLayoutSubtitle.text = context.getString(R.string.recentCreationsLayoutSubtitle_str, pixelArt.width, pixelArt.height)
loadPixelArtStarred(pixelArt)
loadPixelArtTitle(pixelArt)
}
}
Here is the result:
When the user long taps on a project, they get the following dialog:
When they press 'Rename', they get the following dialog where they can rename the project:
My issue is, that when the user types in a new name, and then presses OK, the data is not updating. Sometimes it takes twice to update, sometimes I need to restart the app for it to update, and sometimes it doesn't update at all.
Here is the code responsible for renaming:
fun MainActivity.extendedOnRenameTapped(pixelArt: PixelArt, bottomSheetDialog: BottomSheetDialog) {
val inflatedActivity = activity()?.layoutInflater?.inflate(R.layout.save_file_under_new_name_alert, activity()?.findViewById(android.R.id.content),false)
val textInput: TextInputLayout = inflatedActivity as TextInputLayout
showDialog(
getString(R.string.dialog_rename_title_in_code_str),
null,
getString(R.string.generic_ok_in_code_str), { _, _ ->
val input: String = textInput.editText?.text.toString()
if (input.isNotBlank()) {
pixelArt.title = input
pixelArtViewModel.update(pixelArt)
adapter.submitList(pixelArtData)
bottomSheetDialog.dismiss()
}
}, getString(R.string.generic_cancel_in_code_str), null, view = textInput, dimBackground = false
)
}
I am following everything by the book, so I am confused why this is not working.
Edit
I tried to make it all 'val' and then add this:
pixelArtViewModel.update(pixelArt.copy(title = input))
pixelArtViewModel.getAll().observe(this) {
adapter.submitList(it)
}
bottomSheetDialog.dismiss()
Still not working.
I see that you are setting pixelArt.title, which means your PixelArt class is mutable (has var properties or val properties that reference mutable classes). DiffUtil is 100% incompatible with mutable classes, because they make it impossible to compare items in the old and new lists. It will see the old list as having the new value already so it will treat it as unchanged.
Example with my imagined version of your PixelArt class.
data class PixelArt(
val objId: Long,
val name: String,
val starred: Boolean,
val imageFilePath: String
)
// In ViewModel:
// You probably have the list backed up to disk somehow. I'm just using
// placeholder functions to represent working with the repo or files or
// whatever you use.
val pixelArtLiveData = MutableLiveData<List<PixelArt>>().also {
viewModelScope.launch { it.value = readThePersistedData() }
}
private fun modifyItem(oldItem: PixelArt, newItem: PixelArt) {
pixelArtLiveData.value = pixelArtLiveData.value.orEmpty()
.map { if (it == oldItem) newItem else it }
// also update your persisted data here
}
fun renameItem(originalItem: PixelArt, newName: String) {
modifyItem(originalItem, originalItem.copy(name = newName))
}
fun toggleItemStarred(originalItem: PixelArt) {
modifyItem(originalItem, originalItem.copy(starred = !originalItem.starred))
}
// etc. or you could just make modifyItem public instead of making
// all these helper functions
Then in your adapter, you must call through to these ViewModel functions instead of directly modifying the items or the list or calling submitList. Since the adapter doesn't have direct access to the ViewModel, you probably use your RecentCreationsListener for this by adding
appropriate actions to it that your various click listeners can call.
Your Activity or Fragment would observe this LiveData and simply call submitList() with the observed value.
After creating an activity for a Settings page I noticed that, mCurrentValue !== value in if (mCurrentValue !== value) returned a warning:
Identity equality for arguments of types Boolean? and Boolean can be unstable because of implicit boxing
I tried resolving this warning by adding a ? next to Boolean in override fun onXchange(value:Boolean) {, but then the following error returned:
'onXchange' overrides nothing
Any ideas on how to resolve this issue?
Activity class
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.preference.PreferenceManager
import android.support.v7.app.AppCompatActivity
import android.view.MenuItem
class MySettingsActivity : AppCompatActivity(), MySettingsFragment.PreferenceXchangeListener {
private var mCurrentValue: Boolean? = null
override fun onCreate(savedInstanceState: Bundle?) {
val mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
mCurrentValue = mSharedPreferences.getBoolean("preference_a", false)
if (mCurrentValue as Boolean)
{
setTheme(R.style.MyDarkAppCompatTheme)
}
else
{
setTheme(R.style.MyLightAppCompatTheme)
}
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_mysettings)
val settingsFragment = MySettingsFragment()
supportFragmentManager
.beginTransaction()
.replace(R.id.settings_container, settingsFragment)
.commit()
val myActionBar = actionBar
if (myActionBar != null)
{
myActionBar.setTitle(R.string.settings)
myActionBar.setBackgroundDrawable(ColorDrawable(Color.BLACK))
myActionBar.setDisplayHomeAsUpEnabled(true)
myActionBar.setDisplayShowHomeEnabled(true)
myActionBar.setHomeAsUpIndicator(resources.getDrawable(R.drawable.ic_arrow_back_white, null))
}
}
override fun onXchange(value:Boolean?) {
if (mCurrentValue !== value) {
mCurrentValue = value
recreate()
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
val intent = parentActivityIntent
intent?.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
onBackPressed()
return true
}
else ->
return super.onOptionsItemSelected(item)
}
}
}
Fragment class
class MySettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceChangeListener {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.app_preferences)
val mCheckBoxPreference = findPreference("preference_a") as CheckBoxPreference
mCheckBoxPreference.onPreferenceChangeListener = this
}
private var mPreferenceXchangeListener: PreferenceXchangeListener? = null
interface PreferenceXchangeListener {
fun onXchange(value:Boolean)
}
override fun onAttach(context: Context) {
super.onAttach(context)
try
{
mPreferenceXchangeListener = context as MySettingsFragment.PreferenceXchangeListener
}
catch (e:ClassCastException) {
Log.e(TAG, "onAttach::::: PreferenceXchangeListener must be set in parent Activity")
}
}
override fun onPreferenceChange(preference: Preference, newValue:Any):Boolean {
val preferenceKey = preference.key
if (preferenceKey == "preference_a")
{
(preference as CheckBoxPreference).isChecked = newValue as Boolean
mPreferenceXchangeListener!!.onXchange(newValue)
return true
}
return false
}
companion object {
private val TAG = MySettingsFragment::class.java.simpleName
}
}
So first I can explain the issue it describes. Let's propose a very contrived scenario:
BooleanProvider.java
class BooleanProvider {
#NonNull Boolean wrapMyBoolean(boolean state) {
return new Boolean(state);
}
}
BooleanProviderTest.kt
class BooleanProviderTest {
#Test fun `it returns a true value when true is provided`() {
assertSame(BooleanProvider().wrapMyBoolean(true), true)
}
}
This test will actually fail, because the instances of Boolean are not the same. In the Java code, we initialized a new instance of Boolean (rather than the statically defined Boolean.TRUE and Boolean.FALSE instances that you'll get when a primitive is auto-boxed to a java.lang.Boolean). So to avoid a potential unexpected result, it is recommending that you don't compare these types by reference.
The simplest fix would be to just change your equality test to != instead of !==. This would perform identity equality testing rather than reference equality testing, and will handle a null value on either side appropriately. This is what you want 99% of the time anyway.
Secondly, you can also just declare mCurrentValue as a non-null type, and give it a default value, if you don't need to handle a null value in a specific way. Just declare it as:
private var mCurrentValue: Boolean = false // false is the default here
So I use kotlin for android, and when inflating views, I tend to do the following:
private val recyclerView by lazy { find<RecyclerView>(R.id.recyclerView) }
This method will work. However, there is a case in which it will bug the app. If this is a fragment, and the fragment goes to the backstack, onCreateView will be called again, and the view hierarchy of the fragment will recreated. Which means, the lazy initiated recyclerView will point out to an old view no longer existent.
A solution is like this:
private lateinit var recyclerView: RecyclerView
And initialise all the properties inside onCreateView.
My question is, is there any way to reset lazy properties so they can be initialised again? I like the fact initialisations are all done at the top of a class, helps to keep the code organised. The specific problem is found in this question: kotlin android fragment empty recycler view after back
Here is a quick version of a resettable lazy, it could be more elegant and needs double checked for thread safety, but this is basically the idea. You need something to manage (keep track) of the lazy delegates so you can call for reset, and then things that can be managed and reset. This wraps lazy() in these management classes.
Here is what your final class will look like, as an example:
class Something {
val lazyMgr = resettableManager()
val prop1: String by resettableLazy(lazyMgr) { ... }
val prop2: String by resettableLazy(lazyMgr) { ... }
val prop3: String by resettableLazy(lazyMgr) { ... }
}
Then to make the lazy's all go back to new values on next time they are accessed:
lazyMgr.reset() // prop1, prop2, and prop3 all will do new lazy values on next access
The implementation of the resettable lazy:
class ResettableLazyManager {
// we synchronize to make sure the timing of a reset() call and new inits do not collide
val managedDelegates = LinkedList<Resettable>()
fun register(managed: Resettable) {
synchronized (managedDelegates) {
managedDelegates.add(managed)
}
}
fun reset() {
synchronized (managedDelegates) {
managedDelegates.forEach { it.reset() }
managedDelegates.clear()
}
}
}
interface Resettable {
fun reset()
}
class ResettableLazy<PROPTYPE>(val manager: ResettableLazyManager, val init: ()->PROPTYPE): Resettable {
#Volatile var lazyHolder = makeInitBlock()
operator fun getValue(thisRef: Any?, property: KProperty<*>): PROPTYPE {
return lazyHolder.value
}
override fun reset() {
lazyHolder = makeInitBlock()
}
fun makeInitBlock(): Lazy<PROPTYPE> {
return lazy {
manager.register(this)
init()
}
}
}
fun <PROPTYPE> resettableLazy(manager: ResettableLazyManager, init: ()->PROPTYPE): ResettableLazy<PROPTYPE> {
return ResettableLazy(manager, init)
}
fun resettableManager(): ResettableLazyManager = ResettableLazyManager()
And some unit tests to be sure:
class Tester {
#Test fun testResetableLazy() {
class Something {
var seed = 1
val lazyMgr = resettableManager()
val x: String by resettableLazy(lazyMgr) { "x ${seed}" }
val y: String by resettableLazy(lazyMgr) { "y ${seed}" }
val z: String by resettableLazy(lazyMgr) { "z $x $y"}
}
val s = Something()
val x1 = s.x
val y1 = s.y
val z1 = s.z
assertEquals(x1, s.x)
assertEquals(y1, s.y)
assertEquals(z1, s.z)
s.seed++ // without reset nothing should change
assertTrue(x1 === s.x)
assertTrue(y1 === s.y)
assertTrue(z1 === s.z)
s.lazyMgr.reset()
s.seed++ // because of reset the values should change
val x2 = s.x
val y2 = s.y
val z2 = s.z
assertEquals(x2, s.x)
assertEquals(y2, s.y)
assertEquals(z2, s.z)
assertNotEquals(x1, x2)
assertNotEquals(y1, y2)
assertNotEquals(z1, z2)
s.seed++ // but without reset, nothing should change
assertTrue(x2 === s.x)
assertTrue(y2 === s.y)
assertTrue(z2 === s.z)
}
}
I find a convenient method:
import java.util.concurrent.atomic.AtomicReference
import kotlin.reflect.KProperty
fun <T> resetableLazy(initializer: () -> T) = ResetableDelegate(initializer)
class ResetableDelegate<T>(private val initializer: () -> T) {
private val lazyRef: AtomicReference<Lazy<T>> = AtomicReference(
lazy(
initializer
)
)
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
return lazyRef.get().getValue(thisRef, property)
}
fun reset() {
lazyRef.set(lazy(initializer))
}
}
test:
import org.junit.Assert
import org.junit.Test
class ResetableLazyData {
var changedData = 0
val delegate = resetableLazy { changedData }
val readOnlyData by delegate
}
class ResetableLazyTest {
#Test
fun testResetableLazy() {
val data = ResetableLazyData()
data.changedData = 1
Assert.assertEquals(data.changedData, data.readOnlyData)
data.changedData = 2
Assert.assertNotEquals(data.changedData, data.readOnlyData)
data.delegate.reset()
Assert.assertEquals(data.changedData, data.readOnlyData)
data.changedData = 3
Assert.assertNotEquals(data.changedData, data.readOnlyData)
}
}
I had the same task, and this is what I used:
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
class SingletonLazy<T : Any>(val initBlock: () -> T, val clazz: Class<T>) {
operator fun <R> provideDelegate(ref: R, prop: KProperty<*>): ReadOnlyProperty<R, T> = delegate()
#Suppress("UNCHECKED_CAST")
private fun <R> delegate(): ReadOnlyProperty<R, T> = object : ReadOnlyProperty<R, T> {
override fun getValue(thisRef: R, property: KProperty<*>): T {
val hash = clazz.hashCode()
val cached = singletonsCache[hash]
if (cached != null && cached.javaClass == clazz) return cached as T
return initBlock().apply { singletonsCache[hash] = this }
}
}
}
private val singletonsCache = HashMap<Int, Any>()
fun <T> clearSingleton(clazz: Class<T>) : Boolean {
val hash = clazz.hashCode()
val result = singletonsCache[hash]
if (result?.javaClass != clazz) return false
singletonsCache.remove(hash)
return true
}
inline fun <reified T : Any> singletonLazy(noinline block: () -> T): SingletonLazy<T>
= SingletonLazy(block, T::class.java)
usage:
val cat: Cat by singletonLazy { Cat() }
fun main(args: Array<String>) {
cat
println(clearSingleton(Cat::class.java))
cat // cat will be created one more time
println(singletonsCache.size)
}
class Cat {
init { println("creating cat") }
}
Of course, you may have you own caching strategies.
If you want something very simple, extends Lazy<T> and yet efficient in few lines of code, you could use this
class MutableLazy<T>(private val initializer: () -> T) : Lazy<T> {
private var cached: T? = null
override val value: T
get() {
if (cached.isNull()) {
cached = initializer()
}
#Suppress("UNCHECKED_CAST")
return cached as T
}
fun reset() {
cached = null
}
override fun isInitialized(): Boolean = cached != null
companion object {
fun <T> resettableLazy(value: () -> T) = MutableLazy(value)
}
}
Use it like this:
class MainActivity() {
val recyclerViewLazy = MutableLazy.resettable {
findViewById<RecyclerView>(R.id.recyclerView)
}
val recyclerView by recyclerViewLazy
// And later on
override onCreate(savedInstanceState: Bundle?) {
recyclerViewLazy.reset() /** On next get of the recyclerView, it would be updated*/
}
}
Borrowed partly from
lazy(LazyThreadSafetyMode.NONE) { }
provided in the stlib
you can try this
fun <P, T> renewableLazy(initializer: (P) -> T): ReadWriteProperty<P, T> =
RenewableSynchronizedLazyWithThisImpl({ t, _ ->
initializer.invoke(t)
})
fun <P, T> renewableLazy(initializer: (P, KProperty<*>) -> T): ReadWriteProperty<P, T> =
RenewableSynchronizedLazyWithThisImpl(initializer)
class RenewableSynchronizedLazyWithThisImpl<in T, V>(
val initializer: (T, KProperty<*>) -> V,
private val lock: Any = {}
) : ReadWriteProperty<T, V> {
#Volatile
private var _value: Any? = null
override fun getValue(thisRef: T, property: KProperty<*>): V {
val _v1 = _value
if (_v1 !== null) {
#Suppress("UNCHECKED_CAST")
return _v1 as V
}
return synchronized(lock) {
val _v2 = _value
if (_v2 !== null) {
#Suppress("UNCHECKED_CAST") (_v2 as V)
} else {
val typedValue = initializer(thisRef, property)
_value = typedValue
typedValue
}
}
}
override fun setValue(thisRef: T, property: KProperty<*>, value: V) {
// 不论设置何值,都会被重置为空
synchronized(lock) {
_value = null
}
}
}