How to intercept resource loading globally? - android

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

Related

How to automatically pass an argument to a function when it is called implicitly kotlin

maybe I phrased my question a little strange, but something became interesting to me.
Let's imagine that I have some Extension function:
fun Int.foo() {
TODO()
}
Suppose that I need to pass the context of the Fragment from which I call it to this function, in which case I would do it like this:
fun Int.foo(context: Context) {
TODO()
}
Here we are explicitly passing the Context of our Fragment to the function. However, I'm interested in the question - is it possible to somehow change this function so (or can it be called in some other way) so that I do not have to explicitly pass the Context?
I understand that I could do like this:
fun Fragment.foo() {
var context = this.context
}
...however, I need an Extension function just above Int, so this method is not suitable.
Are there any ways how this can be done?
I guess you're looking for context-dependent declarations that let you combine multiple receiver scopes:
context(Fragment)
fun Int.foo() {
check(context != null) // context is actually Fragments context
}
Keep in mind however this feature is still in experimental state so it requires opt in by adding -Xcontext-receivers to your compiler options.
The Int class is just a class for op with numbers
It doesn't make sense to contain a Context object
It is not possible to get context without passing it to the function
There are other ways, which is to create a static object for the application class
for example
class App : Application() {
companion object {
var app: App? = null
}
init {
app = this;
}
}
and then
fun Int.foo(){
val context=App.app
...
}

How to fix EpoxyModel does not work with PagingDataEpoxyController [Android(Kotlin)]

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!

Making an abstract class in kotlin communicate its responsibilities better to its children

I've been considering a few solutions for reducing boilerplate in a large number of fragments that have the same patterns and workflows.
The best solution I've come up with is to abstract a lot of the standard tasks into an abstract class and then force the implementing fragments to override methods that determine the unique flavor for their particular context.
For the sake of providing an example, here's something I'm making up to illustrate the problem.
abstract class ContainsRepetiveBoilerPlate<T:View,U:InfoForView>:Fragment(){
fun performNinetyPercentOfTheWork(){//tedious stuff that's always the same}
val thingThatChildMightWantToAccess=mutableListOf<U>()
fun upDateUsefullThing(){//update thingThatChildMightWantToAccess}
abstract fun fullFillContractToBindViewToItsInformation(t:T,u:U)
}
Now the fragment will look like this:
class HasATTypeViewPopulatedFromAUTypeDataClass:ContainsRepetitiveBoilerplate<T,U>(){
override fun fullfillContractToBindViewToItsInformation(t:T,u:U){//for every view inside of T, give it values from u}
Setting aside just for one moment the why, of which there are many. I'm interested to know any hows for forcing the child to somehow acknowledge that it's aware of thingThatChildMightWantToAccess.
Likewise, what tools are available to force the child to call performNinetyPercentOfTheWork()?
I've seen a couple of good ideas surrounding making performNinetyPercentOfTheWork() include a call to an abstract fun. But it would be awesome if I could make the linter complain if the child never calls it.
Here's a really really stupid solution that isn't at all serious but would help you understand what I want to achieve.
abstract class ContainsRepetiveBoilerPlate<T:View,U:InfoForView>:Fragment(){
var hasBeenCalled=false
fun performNinetyPercentOfTheWork(){hasbeenCalled=true //do other stuff}
val thingThatChildMightWantToAccess=mutableListOf<U>()
fun upDateUsefullThing(){//update thingThatChildMightWantToAccess}
abstract fun fullFillContractToBindViewToItsInformation(t:T,u:U)
override fun onViewCreated(){
toBeInvokedDuringOnViewCreated()
if (!hasBeenCalled){throw Error("you need to call performNinetyPercentOfTheWork()"}
abstract fun toBeInvokedDuringOnViewCreated()
}
Ok, here's an even stupider example, solely for the sake of trying to illustrate the functionality that I want from the linter.
abstract class ContainsRepetiveBoilerPlate<T:View,U:InfoForView>:Fragment(){
var hasBeenCalled=false
fun performNinetyPercentOfTheWork(){hasbeenCalled=true //do other stuff}
val thingThatChildMightWantToAccess=mutableListOf<U>()
fun upDateUsefullThing(){//update thingThatChildMightWantToAccess}
abstract fun fullFillContractToBindViewToItsInformation(t:T,u:U)
abstract fun acknowledgeThatYouKnowAboutUsefullVariable():Boolean
}
You can see from these examples that I'm not being serious. I just really want for their to be a way for me to make my abstract class be more expressive about what it can provide to its children, and what liberties its taken to handle the child's responsibilities.
Your requirement of wanting the "linter" to complain about the fact that some code does not execute some other code (which is just another way of saying make the compiler complain) would require the compiler to know all possible paths of execution a program can take, and then you run into the halting problem.
So, the best you can hope for is run time errors or warnings. Where you implement something that checks that a particular thing has been done and if it has not error.
Or, make sure it happens with something like the template pattern.
Here is a generic inheritance example:
interface LifeCycleAware {
fun stage1()
}
abstract class LifeCycleTemplate : LifeCycleAware {
/**
* Implementations can access this state
*/
protected var stateFromStage1: String? = null
/**
* A template pattern implementation of the lifecycle hook.
*/
override fun stage1() {
startStage1()
doCustomStage1Stuff()
endStage1()
}
protected abstract fun doCustomStage1Stuff()
private fun startStage1() {
stateFromStage1 = "starting-stage-1"
}
private fun endStage1() {
stateFromStage1 = "ended-stage-1"
}
}
Or a more flexible composition example:
typealias InternalStep = (me: LifeCycleComposition) -> Unit
class LifeCycleComposition(
private val customStage1: InternalStep,
private val startStage1: InternalStep = { me -> me.stateFromStage1 = "starting-stage-1" },
private val endStage1: InternalStep = { me -> me.stateFromStage1 = "ended-stage-1" }
) : LifeCycleAware {
/**
* Implementations can access this state
*/
internal var stateFromStage1: String? = null
/**
* A template pattern implementation of the lifecycle hook.
*/
override fun stage1() {
startStage1(this)
customStage1(this)
endStage1(this)
}
}

How to test reading from/updating file in MVP?

I'm trying to change my app from having no design pattern to using MVP.
Originally I had the following code:
override fun onCreateInputView(): View {
//favoritesData is an instance variable, same with "Gson", "parser", "favorites", and "stringArrayListType"
favoritesData = File(filesDir, "favorites_data.json")
if (favoritesData.exists()) {
favorites = Gson.fromJson(parser.parse(FileReader(favoritesData)), stringArrayListType)
}
}
and
fun updateFavoritesFile() {
favoritesData.writeText(Gson.toJson(favorites))
}
After trying to use MVP I changed the code to:
class AnimeFaceKeyboardPresenter(val view : AnimeFaceKeyboardView, private val model : KeyboardModel = KeyboardModel()) : Presenter {
override fun onCreateInputView() {
model.favorites = view.loadFavoritesFile()
//At some point, call view.updateFavoritesFile(arrayListOf("test","test2"))
}
override fun onStartInputView() {
}
}
and the code in the activity itself to:
override fun loadFavoritesFile() : ArrayList<String> {
val favoritesData = File(filesDir, favoritesFileName)
var favorites = ArrayList<String>()
//"favorites" is no longer an instance variable
if (favoritesData.exists()) {
favorites = Gson.fromJson(parser.parse(FileReader(favoritesData)), stringArrayListType)
}
return favorites
}
override fun updateFavoritesFile(favorites: ArrayList<String>) {
File(filesDir, favoritesFileName).writeText(Gson.toJson(favorites))
}
override fun onCreateInputView(): View {
super.onCreateInputView()
presenter = AnimeFaceKeyboardPresenter(this)
presenter.onCreateInputView()
}
I'm not sure if I'm using MVP correctly, but if I am, how would I go about testing this code. For example - writing a test that calls updateFavoritesFile(arrayListOf("test1","test2")) and uses loadFavoritesFile() to check if the contents is as expected.
Well, you might want to relocate your file read and write to your model (they are associated with data which doesn't really belong in your view).
Then your test consists of instantiating your model object, and testing the methods which can be done without the view and presenter (simplifying the tests).
I would be very tempted to abstract your file as some form of "Repository" object that knows how to read and write strings (but you don't care where or how). You would pass the repository object to your model as a construction property. The advantage of this is that you can create a mock or fake Repository object which you can use to "feed" test data and examine written data, making testing that part of your model a little easier.
Don't forget, your view shouldn't have direct access to your model under MVP .. that would me more like MVC (one of the very few differences between MVP and MVC).

Kotlin, generics, and a function that may have no arguments

I'm trying to genericise the boilerplate around a very common pattern, and Kotlin brings me tantalisingly close.
I've built a class that serves as a listener manager, as follows:
class GenericListenerSupport <EventArgumentType, ListenerFunction: (EventArgumentType) -> Unit> {
private val listeners = mutableListOf<ListenerFunction>()
fun addListener(listener: ListenerFunction) {
listeners.add(listener)
}
fun removeListener(listener: ListenerFunction) {
listeners.remove(listener)
}
fun fireListeners(argument: EventArgumentType) {
listeners.forEach { it.invoke(argument) }
}
}
and it can be used as follows:
class ExampleWithArgument {
private val listenerSupport = GenericListenerSupport<String, (String)->Unit>()
fun exampleAdd() {
listenerSupport.addListener({ value -> System.out.println("My string: "+value)})
}
fun exampleFire() {
listenerSupport.fireListeners("Hello")
}
}
So far, so good. But what if the listener has no arguments? Or stretching even further, multiple parameters.
I can scrape through with this:
class ExampleWithNoArgument {
private val listenerSupport = GenericListenerSupport<Nothing?, (Nothing?)->Unit>()
fun exampleAdd() {
listenerSupport.addListener({ System.out.println("I've got no argument")})
}
fun exampleFiring() {
listenerSupport.fireListeners(null)
}
}
but it smells, and obviously it's no use for multiple parameters.
Is there a better way to pull this off? e.g. something supporting this concept:
private val listenerSupport = GenericListenerSupport<???, (String, Double)->Unit>()
Since your GenericListenerSupport declares a type parameter EventArgumentType and expects an instance of it in fun fireListeners(argument: EventArgumentType), I doubt you can support multiple arguments in a clean way. Instead, I'd suggest using a data class (which is not so much extra code), as a clean and type-safe way to wrap multiple values:
data class MyEvent(val id: String, val value: Double)
private val listenerSupport = GenericListenerSupport<MyEvent, (MyEvent) -> Unit>()
As to passing no value, you can also use Unit, the type that has exactly one value Unit:
listenerSupport.fireListeners(Unit)
The type system and resolution won't allow you to pass no argument where a single one is expected, but, as #Ruckus T-Boom suggested, you can make an extension to fire listeners with no value where Unit is expected:
fun GenericListenerSupport<Unit>.fireListeners() = fireListeners(Unit)
A bit off-topic, but I think you can simplify the type if you don't need custom function types and (EventArgumentType) -> Unit is sufficient:
class GenericListenerSupport<EventArgumentType> {
/* Just use `(EventArgumentType) -> Unit` inside. */
}

Categories

Resources