I want to create a helper object that helps me to access strings from any where of project. Also this helper object should work in unit tests too. But I can not be sure that if is there any risk of memory leak for example usage ?
This is the helper object.
object ResourceHelper {
private var getString: (Int) -> String = {
it.toString()
}
private var getStringWithArgs: (Int, Array<out Any>) -> String = { id, args ->
"$id${args.contentToString()}"
}
fun getString(#StringRes id: Int): String {
return getString.invoke(id)
}
fun getString(#StringRes id: Int, vararg args: Any): String {
return getStringWithArgs.invoke(id, args)
}
fun initialize(resources: Resources) {
getString = { id -> resources.getString(id) }
getStringWithArgs = { id, args -> resources.getString(id, *args) }
}
}
This is the only activity in project.
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
ResourceHelper.initialize(resources)
}
}
This is the view model class, I do not have very detailed knowledge about how references is hold in stack. Is there any problem with this approach ?
class MyViewModel : ViewModel() {
fun printString(id: Int) {
val s = ResourceHelper.getString(id)
Log.d("*****", s)
}
}
The Resources object from an Activity is different each time the Activity is started due to a configuration change.
Your code will temporarily leak the Resources object in between the times when the Activity is destroyed and the next time it is recreated (in onCreate() when you call initialize() again and overwrite which Resources object is being held by reference). If that's just the time during an orientation change, it's trivial. But if it's during the time between the user backing out of the Activity and the next time they return to the app, then your app will be hanging onto a little more memory than necessary.
State-holding singletons like this make unit testing difficult, anyway. I suggest creating extension functions as an alternative. If you need String resources access in a ViewModel, you can extend from AndroidViewModel, which has Resources access through the Application.
fun AndroidViewModel.getString(#StringRes id: Int) =
getApplication<Application>().resources.getString(id)
fun AndroidViewModel.getString(#StringRes id: Int, vararg args: Any) =
getApplication<Application>().resources.getString(id, args)
Related
private fun openTrailer() {
val trailerDialog = Dialog(this)
val trailerDialogBinding = DialogTrailerBinding.inflate(layoutInflater)
trailerDialog.setContentView(trailerDialogBinding.root)
youtubePLayerInit = object : YouTubePlayer.OnInitializedListener {
override fun onInitializationSuccess(
p0: YouTubePlayer.Provider?,
youtubePlayer: YouTubePlayer?,
p2: Boolean
) {
fun loadTrailer2(videoId: String) {
youtubePlayer?.loadVideo(videoId)
}
}
override fun onInitializationFailure(
p0: YouTubePlayer.Provider?,
p1: YouTubeInitializationResult?,
) {
toast("la")
}
}
if (!isInitialized) {
trailerDialogBinding.vvMovieTrailer.initialize(youtubeApiKey, youtubePLayerInit)
isInitialized = true
} else {
Log.e("initializerStatus", "Already Initialized")
}
trailerDialog.show()
}
How do I access access the loadTrailer2 function in the main code? Load Trailer is the function inside the object of the YoutubePlayer.OnInitializedListener. I'm trying to accesss it outside the open trailer function, aka in the onCreate method
Well, as it is right now, loadTrailer2 is a local function declared inside onInitializationSuccess - it only exists while that function is running, so you can't access it from outside that scope.
You could move it into the object itself, but since it relies on the YoutubePlayer object being passed in, how would you call it? Do you have access to that player in onCreate?
If you did have access to it (e.g. when the initialisation first succeeds, you store it in a ViewModel or something, and use that next time you create a fragment) you still have an issue: the type of youtubePlayerInit is YouTubePlayer.OnInitializedListener, and you're adding another method to your object, one that isn't part of that YouTubePlayer.OnInitializedListener interface.
What that means, is nothing actually knows that method exists. If you create that anonymous object inside a function, the compiler sees it as a special type that contains that method, so you can access it directly:
interface Thing {
fun onSomeEvent(code: Int)
}
fun main() {
doSetup()
}
fun doSetup() {
val myThing = object : Thing {
override fun onSomeEvent(code: Int) {
println("Event code: $code")
}
fun coolFunction() {
println("Secret function accessed")
}
}
// calling it inside the scope the object was declared in, so the function
// is visible here
myThing.coolFunction()
}
>> Secret function accessed
but outside of that scope, it just looks like the type declared when creating the object (Thing in this example):
interface Thing {
fun onSomeEvent(code: Int)
}
fun main() {
val myThing = doSetup()
// this only knows it's a Thing, which doesn't have this method
myThing.coolFunction()
}
fun doSetup(): Thing {
val myThing = object : Thing {
override fun onSomeEvent(code: Int) {
println("Event code: $code")
}
fun coolFunction() {
println("Secret function accessed")
}
}
return myThing
}
>> Unresolved reference: coolFunction
If you wanted to access that function, you'd have to e.g. declare a CoolFunctionHaver interface with that function, and either declare myThing as that type, or cast it if you really have to
I am learning Kotlin by trying to build a small app that find and and remember last connected BLE device. To recognize the last connected device I decide to save its MAC address using shared preferences (is that the best way to do that is also a question). I use a tutorial online and it worked well (I didn't remember the page) but today when I open the project to continue the job it gives me error - unresolved reference getSharedPreferences. My question is what is the problem - I get lost :) Here is the class where I have the error row 23.
import android.content.Context
import android.content.SharedPreferences
interface PreferencesFunctions {
fun setDeviceMAC(deviceMAC: String)
fun getDeviceMAC(): String
fun setLastConnectionTime(lastConnectionTime: String)
fun getLastConnectionTime(): String
fun clearPrefs()
}
class PreferenceManager(context: ScanResultAdapter.ViewHolder) : PreferencesFunctions{
private val PREFS_NAME = "SharedPreferences"
private var preferences: SharedPreferences
init {
preferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
}
override fun setDeviceMAC(deviceMAC: String) {
preferences[DEVICE_MAC] = deviceMAC
}
override fun getDeviceMAC(): String {
return preferences[DEVICE_MAC] ?: ""
}
override fun setLastConnectionTime(lastConnectionTime: String) {
preferences[LAST_CONNECTION_TIME] = lastConnectionTime
}
override fun getLastConnectionTime(): String {
return preferences[LAST_CONNECTION_TIME] ?: ""
}
override fun clearPrefs() {
preferences.edit().clear().apply()
}
companion object{
const val DEVICE_MAC = "yyyyyyy"
const val LAST_CONNECTION_TIME = "zzzzzzz"
}
}
Your arguement context is not a acitivity or fragment, and you need those two to call getSharedPreferences method.
class PreferenceManager(context: Context) : PreferencesFunctions{
I have a list with four elements: created_at, text, name, screen_name. The first represent a date of creation, the second the texto of a tweet and the latest the name and screen name of user.
I want to storage this information with lifespan, a random lifespan. For this i thinking using the cache and the implementation of this link https://medium.com/#kezhenxu94/how-to-build-your-own-cache-in-kotlin-1b0e86005591.
My questions is:
use a map key-value and save in value a string with all information (created_at, text, name, screen_name)?
how add this information in map with this code?
Please, give me a sample example for storage this data. Or if there is another way to make what i want more correctly, tell me.
My code in the moment:
class ExpirableCache(private val delegate: Cache, private val flushInterval: Long = TimeUnit.MINUTES.toMillis(1000)) : Cache {
private val dataTweet: Map<Long, Long>? = null
private var lastFlushTime = System.nanoTime()
override val size: Int
get() = delegate.size
override fun set(key: Any, value: Any) {
delegate[key] = value
}
override fun remove(key: Any): Any? {
recycle()
return delegate.remove(key)
}
override fun get(key: Any): Any? {
recycle()
return delegate[key]
}
override fun add(key: Any, value: Any) {
dataTweet[0, value]
}
override fun clear() = delegate.clear()
private fun recycle() {
val shouldRecycle = System.nanoTime() - lastFlushTime >= TimeUnit.MILLISECONDS.toNanos(flushInterval)
if (!shouldRecycle) return
delegate.clear()
}
}
Writting custom views that keep their state across configuration changes in Android is verbose, look at the amount of boilerplate code for saving the state of just one field:
private class SavedState : BaseSavedState {
var amount: Int = 0
constructor(parcel: Parcel) : super(parcel) {
amount = parcel.readInt()
}
constructor (parcelable: Parcelable?) : super(parcelable)
override fun writeToParcel(parcel: Parcel, flags: Int) {
super.writeToParcel(parcel, flags)
parcel.writeInt(amount)
}
companion object CREATOR : Parcelable.Creator<SavedState> {
override fun createFromParcel(parcel: Parcel): SavedState {
return SavedState(parcel)
}
override fun newArray(size: Int): Array<SavedState?> {
return arrayOfNulls(size)
}
}
}
Android Extensions plugin provides the #Parcelize annotation that one can use to autogenerate implementations of Parcelable, but in the case of custom views we have to extend from BaseSavedState not directly from Parcelable.
So, having something like this does not compile:
#Parcelize
data class SavedState(val isLoading: Boolean = false): BaseSavedState()
I am wondering if there's a less verbose way of handling state restoration in a custom view. Would appreciate any ideas or suggestions.
writing a SaveState class is one way to do this that packs all the data that needs to be saved into one class. another way is to just override onSaveInstanceState and onRestoreInstanceState and put your arguments in a bundle. this avoids the boilerplate you mentioned. if you have several arguments just use data class with #Parcelize annotation and save that class using bundle.putParcelable.(instead of the amount in this example). also don't forget to set isSaveEnabled to true.
init { isSaveEnabled = true }
...
override fun onSaveInstanceState(): Parcelable {
val bundle = Bundle()
bundle.putInt("amount", amount)
bundle.putParcelable("superState", super.onSaveInstanceState())
return bundle
}
override fun onRestoreInstanceState(state: Parcelable) {
var viewState = state
if (viewState is Bundle) {
amount = viewState.getInt("amount", 0)
viewState = viewState.getParcelable("superState")
}
super.onRestoreInstanceState(viewState)
}
#Parcelize annotation can still be used because the base class that we have to extend BaseSavedState implements Parcelable. Here is an example where I needed to save some duration in a custom view that I defined.
#Parcelize
internal class SavedState(state: Parcelable?, val duration: Long) : BaseSavedState(state)
As you can see, we can use the annotation to only define the state and the fields that we want to save. Once we have the state ready, we can save/restore it like this:
override fun onSaveInstanceState(): Parcelable {
val parcel = super.onSaveInstanceState()
return SavedState(parcel, currentDuration.timeInMillis)
}
override fun onRestoreInstanceState(state: Parcelable?) {
if (state !is SavedState) {
super.onRestoreInstanceState(state)
return
}
super.onRestoreInstanceState(state.superState)
setDurationInMillis(state.duration)
}
Starting from this implementation, you could add the Parcelable argument for the state class and pass it to the base class. With this, everything should work properly.
Building off #Mohsen's answer, here's the most elegant Kotlin solution I could immediately come up with:
override fun onSaveInstanceState(): Parcelable = bundleOf(
"amount" to amount,
"superState" to super.onSaveInstanceState()
)
override fun onRestoreInstanceState(state: Parcelable) = super.onRestoreInstanceState(
if (state is Bundle) {
amount = state.getInt("amount", 0)
state.getParcelable("superState")
} else {
amount = 0
state
}
)
I have this function in kotlin extension file to pass method but it doesn't work. Please explain me how it make correctly, I try this:
fun showErrorClientScreen(context: Context, action : () -> Unit) {
val intent = Intent(context, RestClientErrorActivity::class.java)
val bundle = Bundle()
bundle.putSerializable(UPDATE_CLIENT_ERROR, ErrorClientListener { action })
intent.putExtra(UPDATE_CLIENT_ERROR_BUNDLE, bundle)
context.startActivity(intent)
}
use java interface
public interface ErrorClientListener extends Serializable {
void tryAgainFunction();
}
and my activity where i need listen click button and try again send request:
class RestClientErrorActivity: BaseActivity(), View.OnClickListener {
private lateinit var errorClientListener: ErrorClientListener
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_rest_client_error)
try {
val bundle = intent.getBundleExtra(UPDATE_CLIENT_ERROR_BUNDLE)
errorClientListener = bundle?.getSerializable(UPDATE_CLIENT_ERROR) as ErrorClientListener
} catch (e: Exception) {
e.message
}
}
override fun onClick(v: View?) {
when (v?.id) {
R.id.ib_update -> errorClientListener.tryAgainFunction()
}
}
}
It is quite strange to package interfaces between activities and it is definitely not advisable. One reason why it is maybe not serializing between Activity A and Activity B is because the object was created in Activity A, it is treated as anonymous class creation and Activity A holds the reference to this object, hence preventing it from being serialised. This is good, because you can create references to objects within the interface callback whose reference in turn would be held by class instantiating it. Therefore, garbage collector won't be able to run collections on these objects and free up the space; causing a massive memory leak.
The alternative approach to your problem could be using clean architectures and a Singleton class pattern that is accessible by both activities and instantiated only once by say Activity A:
class SingletonErrorHandler private constructor(){
var isError = false
fun doOnError() {
// do non view related stuff
// like a network call or something
}
companion object {
val instance by lazy { SingletonErrorHandler() }
}
}
in the activity you can define
class ActivityA : AppCompatActivity() {
fun onError() {
SingletonErrorHandler.instance.isError = true
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.a_activity)
}
}
in activity B
class ActivityB : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.b_activity)
val errorHandler = SingletonErrorHandler.instance
if(errorHandler.isError)
errorHandler.doOnError()
}
}
You can write factory method to start the activity like android studio generates factory method for fragment creation.
class RestClientErrorActivity : AppCompatActivity() {
companion object {
private var completion: (() -> Unit)? = null
fun start(context: Context, completion: (() -> Unit)?) {
RestClientErrorActivity.completion = completion
val bundle = Bundle()
intent.putExtra(UPDATE_CLIENT_ERROR_BUNDLE, bundle)
context.startActivity(intent)
}
}
private lateinit var retryButton: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retryButton = findViewById(R.id.btn_retry)
}
fun onRetryClick(view: View) {
finish()
completion?.invoke()
}
}
Note: completion is not mandatory. so i made that as nullable. if you start activity without using factory method app will not crash.
I had the same problem. As mentioned in HawkPriest's Answer, your object is not serializable, because its an anonymous class. Another way to fix this is to simply implement a non-anonymous class that implements your interface. Here is my code:
Interface
interface MyInterface : Serializable {
fun instruction()
}
Class
class MyClass : MyInterface {
override fun instruction() {
// does something
}
}
Calling Activity
val myObject = MyClass()
val intent = Intent(context, MyActivity::class.java).putExtra("Tag", myObject)
context.startActivity(intent)
Activity
override fun onCreate(savedInstanceState: Bundle?) {
val myObject = intent.getSerializableExtra("Tag") as MyInterface
myObject.instruction()
}
Regarding the "native resources" as mentioned in your comment, you can make your instruction take parameters or pass them to your MyObject.
P.S. The problems I have with the Singleton solution:
Singleton is not eligable for garbage collection, which means it lives on after its not needed anymore. (not 100% sure about that, but that's what I get from this answer)
Using singleton would mean you cant have "multiple different uses" for your activity. If an interface is used, it is to be able to use multiple different implementations of that interface. A singleton wouldn't provide that, without using an interface architecture within your singleton, which would then again render it unnecessary, considering my proposed solution.