Kotlin easily access resources outside of an activity - android

I'm new at Android and Kotlin. I'm trying to create a ResourcesHelper class to easily access my custom colors and fonts from any other custom class in my app. But in this helper I don't have any context. I've read ways to get the context extending the Application class but then compiler says I can't access this context in my ResourcesHelper companion object as it would create memory leaks. Also I ended up with optional chained.
Here is how I would like to be able to use it :
class ResourcesHelper {
companion object {
val lightBlue = resources.getColor(R.color.lightBlue)
val customBlue = resources.getColor(R.color.customBlue)
// [...]
val fontAwesome = resources.getFont(R.font.fontawesome)
val lemonMilk = resources.getFont(R.font.lemonmilk)
}
}
enum class ButtonStyle {
MENU,
// [...]
VICTORY
}
class CustomButton(c: Context, attrs: AttributeSet) : Button(c, attrs) {
var isButtonActivated = false
fun setStyle(style: ButtonStyle) {
setBackgroundColor(ResourcesHelper.transparent)
when(style) {
ButtonStyle.MENU -> {
setText(R.string.menu_button)
typeface = ResourcesHelper.lemonMilk
setBackgroundColor(ResourcesHelper.customRed)
setTextColor(ResourcesHelper.white)
}
// [...]
ButtonStyle.VICTORY -> {
setText(R.string.victory_button)
typeface = ResourcesHelper.lemonMilk
setBackgroundColor(ResourcesHelper.customRed)
setTextColor(ResourcesHelper.white)
}
}
}
}
I also read this post Android access to resources outside of activity but it's in Java and I have no idea on how to do it in Kotlin.
I am completely lost on what and how to do this... Or if there is a better way to achieve reaching resources from anywhere.
Thanks for your help

For Color, String, etc system resources you can use the Resources class, as shown below
import android.content.res.Resources
class ResourcesHelper {
companion object {
val lightBlue = Resources.getSystem().getColor(R.color.lightBlue)
}
}

If you want to support different Android versions I'll recommend to use ContextCompat. It provides unified interface to access different resources and backward compatibility for older Android versions.
For AnroidX use androidx.core.content.ContextCompat, for SupportV4: android.support.v4.content.ContextCompat.
val lightBlue = ContextCompat.getColor(context, R.color.lightBlue)
val customBlue = ContextCompat.getColor(context, R.color.customBlue)

Related

Android + Kotlin + Hilt: Injecting into static methods classes

I'm not very clear about the best way to inject into a static methods helper class (lets say a Custom class).
I'm kinda new to Kotlin, and as I've learnt we can access a method statically in two ways:
Object class.
Class + companion object.
To start, I'm not sure which one is the most recommended one (if there is a best practice regarding this), but my "problem" arises when needing to inject dependencies into a static method class.
Let's go with a simple example:
I have a static methods class called AWUtils (not decided if it should be an object class or a class with companion object yet though, and this will most likely depend on the injection mechanism recommended) with the next method:
fun setAmpersand2Yellow(text2Replace: String, target: String): String {
return text2Replace.replace(
target, "<span style=\"color:" +
app.drawerFooterColor + ";\">" + target + "</span>"
)
}
Here, app is the instance of my AppSettings class which holds all app configuration so, as you see setAmpersand2Yellow needs AppSettings, and of course I would't pass it as a parameter by any means, so it's a AWUtils dependence.
Using AWUtils as a class with companion object for the static methods I cannot inject directly AppSettings into company object as far as I know (at least I cannot do constructor injection, let me know if I'm wrong) and if I inject into companion object parent class (AWUtils) constructor then I don't know how to access those dependences from the companion object itself (the child).
If I use fields injection in AWUtils as a class then it complains than lateinit field has not been initialised and I don't know how to deal with this, because as far as I know lateinit fields are initialised in onCreate, which does not exist in this kind of classes.
One other possibility is to use an object with fields and set the dependencies values from caller in a static way before calling the method, for example:
object AWUtils {
var app: AppSettings? = null
fun setAmpersand2Yellow(text2Replace: String, target: String): String {
return text2Replace.replace(
target, "<span style=\"color:" +
app.drawerFooterColor + ";\">" + target + "</span>"
)
}
}
#AndroidEntryPoint
class OtherClass
#Inject constructor(private val app: AppSettings) {
fun AnyFunction() {
var mystr = "whatever"
AWUtils.app = app
var yellowStr = AWUtils.setAmpersand2Yellow(myStr)
}
}
In the end, I'm not sure on how to supply dependencies to a static methods class and which form of "static" class should I choose.
Edit 1:
Apart from my ApSettings class, I need a context, like for example in this next isTablet method:
val isTablet: String
get() {
return ((context.resources.configuration.screenLayout
and Configuration.SCREENLAYOUT_SIZE_MASK)
>= Configuration.SCREENLAYOUT_SIZE_LARGE)
}
In the end, I need a context and my AppSettings (or any other custom classes) to be injected anyway in a class with static methods.
Edit 2:
I could do (from the activity):
AWUtils.context = this
AWUtils.app = app
var isTablet = AWUtils.isTablet
And it works, but rather to be in the need of assigning a value to two fields (or more) every time I need to call a static method, I would prefer the fields to be injected in any way.
That's what dependency injection is meant for, isn't it?
Edit 3: I'm starting to be fed up with Hilt, what is supposed would have been created to simplify our life, only makes our programming life much more complicated.
As you clarified in the comments, you want to have your utils class accessible in an easy way across your codebase, so this answer will focus on that and on your original questions.
I'm kinda new to Kotlin, and as I've learnt we can access a method statically in two ways: Object class or Class + companion object.
Kotlin does not have Java-style statics. One reasoning behind it was to encourage more maintainable coding practices. Static methods and static classes are also a nightmare for testing your code.
In Kotlin you would go with an object (but a class + companion object would work in the same way)
object AWUtils {
lateinit var appContext: Context
lateinit var appSettings: AppSettings
fun initialize(
appContext: Context,
appSettings: AppSettings,
// more dependencies go here
) {
this.appContext = appContext
this.appSettings = appSettings
// and initialize them here
}
val isTablet: Boolean
get() = ((appContext.resources.configuration.screenLayout
and Configuration.SCREENLAYOUT_SIZE_MASK)
>= Configuration.SCREENLAYOUT_SIZE_LARGE)
fun setAmpersand2Yellow(text2Replace: String, target: String): String {
return text2Replace.replace(
target, "<span style=\"color:" +
appSettings.drawerFooterColor + ";\">" + target + "</span>"
)
}
}
Since this object should be accessible across the whole application it should be initialized as soon as possible, so in Application.onCreate
#HiltAndroidApp
class Application : android.app.Application() {
// you can inject other application-wide dependencies here
// #Inject
// lateinit var someOtherDependency: SomeOtherDependency
override fun onCreate() {
super.onCreate()
// initialize the utils singleton object with dependencies
AWUtils.initialize(applicationContext, AppSettings())
}
Now anywhere in your app code you can use AWUtils and AppSettings
class OtherClass { // no need to inject AppSettings anymore
fun anyFunction() {
val mystr = "whatever"
val yellowStr = AWUtils.setAmpersand2Yellow(myStr)
// This also works
if (AWUtils.isTablet) {
// and this as well
val color = AWUtils.appSettings.drawerFooterColor
}
}
}
There is another way in Kotlin to write helper/util functions, called extension functions.
Your isTablet check might be written as an extension function like this
// This isTablet() can be called on any Configuration instance
// The this. part can also be omitted
fun Configuration.isTablet() = ((this.screenLayout
and Configuration.SCREENLAYOUT_SIZE_MASK)
>= Configuration.SCREENLAYOUT_SIZE_LARGE)
// This isTablet() can be called on any Resources instance
fun Resources.isTablet() = configuration.isTablet()
// This isTablet() can be called on any Context instance
fun Context.isTablet() = resources.isTablet()
With the above extension functions in place the implementation inside AWUtils would be simplified to
val isTablet: Boolean
get() = appContext.isTablet()
Inside (or on a reference of) any class that implements Context, such as Application, Activity, Service etc., you can then simply call isTablet()
class SomeActivity : Activity() {
fun someFunction() {
if (isTablet()) {
// ...
}
}
}
And elsewhere where Context or Resources are available in some way, you can simply call resources.isTablet()
class SomeFragment : Fragment() {
fun someFunction() {
if (resources.isTablet()) {
// ...
}
}
}
Edit 3: I'm starting to be fed up with Hilt, what is supposed would have been created to simplify our life, only makes our programming life much more complicated.
Yeah, Hilt is focusing on constructor injection and can only do field injection out-of-the-box in very limited cases, afaik only inside Android classes annotated with #AndroidEntryPoint and inside the class extending the Application class when annotated with #HiltAndroidApp.
Docs for #AndroidEntryPoint say
Marks an Android component class to be setup for injection with the standard Hilt Dagger Android components. Currently, this supports activities, fragments, views, services, and broadcast receivers.
If you feel that you need a lot of field injection, because you are working with "static"-like objects in Kotlin, consider using Koin instead of Hilt for your next project.

Is it possible to get a context in a CompositionLocalProvider?

I've got some configuration in a json file stored at the asset folder of the app.
I need this configuration during my complete app so I thought a CompositionLocalProvider may be a good choice.
But now I realize I need the context for parsing that json file and that doesn't seem to be possible.
Might there be another way to achieve the goal I'm looking for?
This is my implementation so far:
val LocalAppConfiguration = compositionLocalOf {
Configuration.init(LocalContext.current) // <-- not possible
}
Where my Configuration is like:
object Configuration {
lateinit var branding: Branding
fun init(context: Context) {
val gson = GsonBuilder().create()
branding = gson.fromJson(
InputStreamReader(context.assets.open("branding.json")),
Branding::class.java
)
}
}
I would be very grateful if someone could help me further
compositionLocalOf is not a Composable function. So, LocalContext.current can not be used.
I believe you can achieve the similar goal if you move the initialization of branding outside of the default factory. You'd then do the initialization inside your actual composable where you have access to Context.
Here's a sample code to explain what I'm talking about.
val LocalAppConfiguration = compositionLocalOf {
Configuration
}
#Composable
fun RootApp(
isDarkTheme: Boolean = isSystemInDarkTheme(),
content: #Composable () -> Unit
) {
val brandedConfiguration = Configuration.init(LocalContext.current)
MaterialTheme {
CompositionLocalProvider(LocalAppConfiguration provides brandedConfiguration) {
//your app screen composable here.
}
}
}
Note that you will also have to modify your init method slightly.
object Configuration {
lateinit var branding: Branding
fun init(context: Context) : Configuration {
val gson = GsonBuilder().create()
branding = gson.fromJson(
InputStreamReader(context.assets.open("branding.json")),
Branding::class.java
)
return this
}
}

How to intercept resource loading globally?

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

Dependency Injection on Arrow KT

In Arrow Kt Documentation on Dependency Injection, the dependency is defined at the "Edge of the World" or in Android could be an Activity or a Fragment. So the given example is as follow:
import Api.*
class SettingsActivity: Activity {
val deps = FetcherDependencies(Either.monadError(), ActivityApiService(this))
override fun onResume() {
val id = deps.createId("1234")
user.text =
id.fix().map { it.toString() }.getOrElse { "" }
friends.text =
deps.getUserFriends(id).fix().getOrElse { emptyList() }.joinToString()
}
}
But now I'm thinking how could the SettingsActivity in the example could be unit tested? Since the dependency is created within the activity, it could no longer be changed for testing?
When using some other Dependency Injection library, this dependency definition is create outside of the class it will be used on. For example in Dagger, a Module class is created to define how the objects (dependencies) are created and an #Inject is used to "inject" the dependency defined inside the module. So now when unit testing the Activity, I just have to define a different module or manually set the value of the dependency to a mock object.
In Dagger you would create a Mock or Test class that you would #Inject instead of ActivityApiService. It is the same here.
Instead of:
class ActivityApiService(val ctx: Context) {
fun createId(): String = doOtherThing(ctx)
}
You do
interface ActivityApiService {
fun createId(): String
}
and now you have 2 implementations, one for prod
class ActivityApiServiceImpl(val ctx: Context): ActivityApiService {
override fun createId(): Unit = doOtherThing(ctx)
}
and another for testing
fun testBla() {
val api = object: ActivityApiService {
override fun createId(): String = "4321"
}
val deps = FetcherDependencies(Either.monadError(), api)
deps.createId("1234") shouldBe "4321"
}
or even use Mockito or a similar tool to create an ActivityApiService.
I have a couple of articles on how to decouple and unitest outside the Android framework that aren't Arrow-related. Check 'Headless development in Fully Reactive Apps' and the related project https://github.com/pakoito/FunctionalAndroidReference.
If your dependency graph becomes too entangled and you'd like some compile-time magic to create those dependencies, you can always create a local class in tests and #Inject the constructor there. The point is to decouple from things that aren't unitestable, like the whole Android framework :D

Kotlin: How to access field from another class?

package example
class Apple {
val APPLE_SIZE_KEY: String = "APPLE_SIZE_KEY"
}
Class:
package example
class Store {
fun buy() {
val SIZE = Apple.APPLE_SIZE_KEY
}
}
Error:
'APPLE_SIZE_KEY' has private access in 'example.Apple'
But official documentation describes that if we do not specify any visibility modifier, public is used by default.
Why is above error coming?
What you are trying to do is accessing a value of a class that has no instance. Here are three solutions:
package example
object Apple {
val APPLE_SIZE_KEY: String = "APPLE_SIZE_KEY"
}
This way you do not need to instantiate anything because of the way objects work in Kotlin.
You could also just instantiate your class like this:
package example
class Store {
fun buy() {
val SIZE = Apple().APPLE_SIZE_KEY
}
}
In this solution you also have an object of Apple, but Apple is still declared as a class.
The third option is a companion object, which behaves like static variables in Java.
package example
class Apple {
companion object {
val APPLE_SIZE_KEY: String = "APPLE_SIZE_KEY"
}
}
If you want this to be a class level property instead of an instance level property, you can use a companion object:
class Apple {
companion object {
val APPLE_SIZE_KEY: String = "APPLE_SIZE_KEY"
}
}
fun useAppleKey() {
println(Apple.APPLE_SIZE_KEY)
}
What you currently have is an instance property, which you could use like this:
fun useInstanceProperty() {
val apple = Apple()
println(apple.APPLE_SIZE_KEY)
}

Categories

Resources