I am looking for utility or any way how can I measure the time required for inflater to inflate some portion (view or viewgroup or whole screen) in Android.
I know there is default feature in debug mode that shows overdraw, but I need to have measure the time taken to inflate view.
What is the best way to do this.
Thx.
Check out lucasr/probe. Here is a blog post by a Facebook engineer on improving view measure time.
Hierarchy Viewer is also a useful visual tool to see which part of your layout can be improved.
In 2019, you can use android x benchmark library easy for benchmark inflate time.
android doc write-benchmark
#RunWith(AndroidJUnit4::class)
class ViewBenchmark {
#get:Rule
val benchmarkRule = BenchmarkRule()
#Test
fun simpleViewInflate() {
val context = ApplicationProvider.getApplicationContext()
val inflater = LayoutInflater.from(context)
val root = FrameLayout(context)
benchmarkRule.keepRunning {
inflater.inflate(R.layout.test_simple_view, root, false)
}
}
}
or you can use system trace to benchmark only inflate layout code
In 2022, you have androidx.benchmark library where you can measure layout inflation time. Check out the sample code here from Google. https://github.com/android/performance-samples
ViewInflateBenchmark.kt
#RunWith(AndroidJUnit4::class)
class ViewInflateBenchmark {
#get:Rule
val benchmarkRule = BenchmarkRule()
#Test
fun benchmarkViewInflate() {
val context = InstrumentationRegistry.getInstrumentation().context
val inflater = LayoutInflater.from(context)
val root = FrameLayout(context)
benchmarkRule.measureRepeated {
val inflated = inflater.inflate(R.layout.item_card, root, false)
}
}
}
Related
Background
I need to use a translations-SDK (Lokalise, docs here) that is intended to load strings resources from their servers.
This means that if you use getString , it will prefer what's on the server instead of what's on the app. This includes also the cases of inflation of layout XML files.
The problem
It seems that Android doesn't have a global resource handling that I can use. This is why the SDK says I should use one of these :
For Activity, I can override the callback of attachBaseContext.
For all other cases, that I need to get the resources of them, I can use LokaliseResources(context) .
Thing is, a lot of code in the app I work on doesn't involve an Activity. A lot of the UI on the app is floating (using SAW permission, AKA "System Alert Window").
This means that there is a lot of inflation of Views using just the Application class.
What I've tried
First I made a simple manager for this:
object TranslationsManager {
var resources: LokaliseResources? = null
#UiThread
fun initOnAppOnCreate(context: App) {
Lokalise.init(context, Keys.LOCALISE_SDK_TOKEN, Keys.LOCALISE_PROJECT_ID)
Lokalise.updateTranslations()
resources = LokaliseResources(context)
}
fun getResources(context: Context): Resources {
return resources ?: context.resources
}
}
I tried to perform various things using the library, but they crashed as it's not how the library works.
So these failed:
For the getResources of the class that extends Application, I tried to return the one of the SDK
Use attachBaseContext of the class that implements Application. This causes a crash since it needs to be initialized before, so I tried to initialize it right in this callback, but still got a crash.
For LayoutInflater, I tried to use LayoutInflater.from(new ContextThemeWrapper(...)) , and override its getResources callback, but it didn't do anything.
I tried to use Philology library by having this:
object MyPhilologyRepositoryFactory : PhilologyRepositoryFactory {
override fun getPhilologyRepository(locale: Locale): PhilologyRepository {
return object : PhilologyRepository {
override fun getPlural(key: String, quantityString: String): CharSequence? {
Log.d("AppLog", "getPlural $key")
return TranslationsManager.resources?.getString(quantityString)
?: super.getPlural(key, quantityString)
}
override fun getText(key: String): CharSequence? {
Log.d("AppLog", "getText $key")
return TranslationsManager.resources?.getString(key) ?: super.getText(key)
}
override fun getTextArray(key: String): Array<CharSequence>? {
Log.d("AppLog", "getTextArray $key")
TranslationsManager.resources?.getStringArray(key)?.let { stringArray ->
val result = Array<CharSequence>(stringArray.size) { index ->
stringArray[index]
}
return result
}
return super.getTextArray(key)
}
}
}
}
And on the class that extends Application, use this:
Philology.init(MyPhilologyRepositoryFactory)
ViewPump.init(ViewPump.builder().addInterceptor(PhilologyInterceptor).build())
But when inflation was used in the app (and actually everywhere), I never saw that this code is being used, ever.
That being said, this is what I've succeeded:
1.For all Activities/Services, indeed I've added usage of attachBaseContext as the SDK says:
override fun attachBaseContext(newBase: Context) {
super.attachBaseContext(LokaliseContextWrapper.wrap(newBase))
}
2.For all custom views, I've used what I've made:
override fun getResources(): Resources {
return TranslationsManager.getResources(context)
}
Both of these took quite some time to find and add manually, one after another.
Sadly, still there seem to be some important cases.
I've found that at least for layout inflation (in the custom views, for example), the layout XML files don't take the resources from the SDK.
I've found an article "Taming Android Resources and LayoutInflater for string manipulation" from 2020 (cache here) saying I could use some trick of ContextThemeWrapper a bit more complex than what I tried, but sadly it lacks some important information (implementation of cloneInContext for example) that I've failed to use:
class CustomContextWrapper(
private val base: Context,
private val dynamicStringMap: Map<String, String>
) : ContextWrapper(base) {
override fun getResources() = CustomResources(base.resources, dynamicStringMap)
override fun getSystemService(name: String): Any? {
if (Context.LAYOUT_INFLATER_SERVICE == name) {
return CustomLayoutInflater(LayoutInflater.from(baseContext), this)
}
return super.getSystemService(name)
}
}
class CustomLayoutInflater constructor(
original: LayoutInflater,
newContext: Context,
) : LayoutInflater(original, newContext) {
override fun cloneInContext(p0: Context?): LayoutInflater {
TODO("Not yet implemented")
}
override fun onCreateView(name: String, attrs: AttributeSet): View? {
try {
val view = createView(name, "android.widget.", attrs)
if (view is TextView) {
// Here we get original TextView and then return it after overriding text
return overrideTextView(view, attrs)
}
} catch (e: ClassNotFoundException) {
} catch (inflateException: InflateException) {
}
return super.onCreateView(name, attrs)
}
private fun overrideTextView(view: TextView, attrs: AttributeSet?): TextView {
val typedArray =
view.context.obtainStyledAttributes(attrs, intArrayOf(android.R.attr.text))
val stringResource = typedArray.getResourceId(0, -1)
view.text = view.resources.getText(stringResource)
typedArray.recycle()
return view
}
}
However, it said I could use a library called "ViewPump" (here, and it actually suggested to use Philology library here) that will do the trick for me, and that from Android 30 we could use ResourcesProvider and ResourcesLoader classes. Sadly I couldn't find an example to use any of these for the purpose I'm working on.
The questions
Is it really possible to use the trick that was mentioned on the article? What should be done to use it properly?
How can I use the "ViewPump"/"Philology" library to achieve the same thing?
Is there any way to offer resources globally instead of using all that I've mentioned? So that all resources will be using the translation SDK, no matter where and how I reach the resources ? This takes a lot of time already, as I need to go over many classes and add handling of resources myself...
Will any of the above cover all cases? For example not just the inflation, but other cases such as TextView.setText(resId) ?
As for the new classes of Android API 30, because they are very new, I've decided to ask about them in a new post, here.
EDIT: Talking with Lokalise support, they said they already do use ViewPump, which means that it probably works in cases that don't match what I have.
I've found success with a combination of using ViewPump to wrap the context of the view being inflated with your ContextWrapper.
class ContextWrappingViewInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): InflateResult {
val request = chain.request()
val newRequest = request.toBuilder()
.context(MyContextWrapper.wrap(request.context))
.build()
return chain.proceed(newRequest)
}
}
However I haven't found a solution to force custom view attributes to use your context for free. The issue is that internally, styled attributes fetch their resources from what has already been cached internally via XML files. Meaning, the view's context doesn't come into it at all.
A workaround for this is to fetch the resource ID from styled attributes and then delegate the actual resource fetching to context.
fun TypedArray.getStringUsingContext(context: Context, index: Int): String? {
if (hasValue(index)) {
return getResourceId(index, 0)
.takeIf { it != 0 }
?.let { context.getString(it) }
}
return null
}
Usage in CustomView:
init {
context.obtainStyledAttributes(attrs, R.styleable.CustomView).use { array ->
val myText = array.getStringUsingContext(context, R.styleable.CustomView_myText)
...
}
}
I have an existing view that extends from ConstraintLayout which looks something like this:
class LandingTemplate: ConstraintLayout {
init {
inflate(context, R.layout.landing_template, this)
// Currently this 'recyclerView' is a kotlin synthetic
recyclerView.run {
// this sets up the recycler view
}
}
I'm familiar with view binding with activities and fragments, but I can't find any documentation around the extends layout case.
My question is, what do I replace that initial inflate call with here?
I'm assuming you have a context available from your constructor and your XML layout's top level tag is <merge>. You can use your binding class's inflate to create and add the child layout.
And since this can all be set up in the constructor, you don't need lateinit var like in the Activity/Fragment examples, and can just use val instead.
class LandingTemplate(context: Context, attrs: AttributeSet): ConstraintLayout(context, attrs) {
private val binding = LandingTemplateBinding.inflate(LayoutInflater.from(context), this)
init {
binding.recyclerView.run {
// this sets up the recycler view
}
}
}
you can get layout inflater like below
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val view = inflater.inflate(R.layout.landing_temple,this,true)
and you must have valid view construct too
LandingTemple(Context) // for creating view programmatically
LandingTemple(Context,AttrributeSet) // to inflate view from xml , and
//the constructor context is one that you use to call `getSystemService
for more information check
I'm trying to use Robinhood Spark in my app but my entire UI is built using jetpack compose. I'm going through these docs but it's only mentioned how you can use XML resource files like String, Dimensions, Colors, Images, Vector drawbles, Icons, and Fonts. I don't know which one Spark falls under. Looking at the XML code it's a LinearLayout but I can't see how that falls under any of the resource types I've mentioned prior. So for now I'm going through non-compose documentations to see if there is some kind of class or method I can use but would appreciate some feedback and help.
It is as nglauber mentioned, but you would need to define layout parameters that will fit into your compose view as well.
More about it https://foso.github.io/Jetpack-Compose-Playground/viewinterop/androidview/
AndroidView(factory = { ctx ->
val view = LayoutInflater.from(ctx)
.inflate(R.layout.activity_sample, null, false)
.apply {
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
}
// Use your view as usual...
view
}, update = {
// here update your view or get element from it and update
})
You can use AndroidView to load your XML file.
AndroidView(
factory = { context: Context ->
val view = LayoutInflater.from(context)
.inflate(R.layout.your_layout, null, false)
// Use your view as usual...
view // return the view
},
update = { view ->
// Update view if needed
}
)
TextView text changes but does not update in layout. I tried every method I could find but nothing worked. I have a very basic application with a single activity and 3 layouts*.
*This is the first app I make so I tought it would have been simpler this way
The main problems I am facing are two: almost all the informations around are old and in java, and my textView text does not change.. The app is a simple Rock-Paper-Scissor game I'm trying to make as an exercise.
The textViews.text values get updated but the layout always shows the same text...
I have no idea what could be the problem. I am also struggling to understand exactly how all of this is working exactly...like InflateLayout, Context and Android in general. I do not understand much from android's reference.
THERE IS NO INFLATE(), POSTINFLATE(), FORCELAYOUT(), VISIBILITY TOGGLES BECAUSE NONE OF THEM WORKED :(
Excerpt of the code
class MainActivity : AppCompatActivity() {
lateinit var TITLE:TextView
lateinit var PARAGRAPH:TextView
override fun onCreate(savedInstanceState :Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val InflaterInitializer = LayoutInflater.from(applicationContext) as LayoutInflater
val inflater = InflaterInitializer.inflate(R.layout.activity_2, null, false)
TITLE= inflater.findViewById(R.id.title) as TextView
PARAGRAPH= inflater.findViewById(R.id.paragraph) as TextView
}
There are three functions like this:
fun FUNCTION(v :View) {
val userChoice = "XXX"
val computerChoice = getComputerChoice()
if (userChoice == computerChoice) {
FUNCTION_2(computerChoice)
} else {
runOnUiThread {
TITLE.text =
if (computerChoice == "YYY") getString(R.string.YOU_WON) else getString(R.string.YOU_LOSE);
PARAGRAPH.text = getString(R.string.STRING, computerChoice)
}
}; resultScreen()
}
Function_2...
private fun FUNCTION_2(cc :String) {
runOnUiThread {
TITLE.text = getString(R.string.STRING)
PARAGRAPH.text = getString(R.string.STRING, cc)
}; resultScreen()
}
resultScreen() is just a call to setContentView(LAYOUT)
Here's a video of the app and the update problem:
https://imgur.com/a/iWCRMkq
Code complete here: https://github.com/noiwyr/MorraCinese
EDIT
Unfortunately none of the answers actually worked as I hoped, however redesigning the app and using multiple activities with some tweaks solved the issue. You may find the new code in the github repo.
However I would be curious to know if there is a working solution for this question :)
By calling InflaterInitializer.inflate(R.layout.activity_2, null, false) you inflate a new view hierarchy from the specified xml resource, which is not attached to any of your views (these new views are not shown on your screen). Then you found text views from that new view hierarchy and changed their titles.
So, your onCreate method have to look like this:
override fun onCreate(savedInstanceState :Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_2)
TITLE = findViewById(R.id.title)
PARAGRAPH = findViewById(R.id.paragraph)
}
Also, it's redundant to use methods runOnUiThread() (your code already runs on Ui thread) and resultScreen().
You no need anything , you creat over code no problem I suggest you
val InflaterInitializer = LayoutInflater.from(applicationContext) as LayoutInflater val inflater = InflaterInitializer.inflate(R.layout.activity_outcome, null, false)
Comment this above code no need in kotlin
motivoRisultato.text = getString(R.string.scelta_pc, computerChoice)
Simpaly make this type of code
There are quite a few errors in your code, so I'm going to break down the answer with your code. Do find the Comments inline
class MainActivity : AppCompatActivity() {
/**
* First, Follow conventions for naming variables, they are usually in camelcase for variables and functions, Capitalized for Constants.
* Second, lateinit is used to defer the initialization of a variable, for views, such as
* TextView's, you could use the Kotlin Synthentic library which automatically references the Views of your layout.
*/
lateinit var TITLE:TextView
lateinit var PARAGRAPH:TextView
override fun onCreate(savedInstanceState :Bundle?) {
super.onCreate(savedInstanceState)
/**
* Set content view, internally set's the layout file after inflation using the Activity context. Which means, that you do not
* need to specifically inflate the view.
*/
setContentView(R.layout.activity_main)
/**
* This is the reason why your layout doesn't know refresh, what you're doing here is inflating another layout, but not setting it to your activity.
* This is not required as explained above
*/
val InflaterInitializer = LayoutInflater.from(applicationContext) as LayoutInflater
/**
* Inflater inflates a View Object. one would use this approach if they were programatically adding Views
*/
val inflater = InflaterInitializer.inflate(R.layout.activity_2, null, false)
/**
* the below views are pointing to a reference of TextView for the non visible inflated view. Which is the reason why the text is not updated.
*/
TITLE= inflater.findViewById(R.id.title) as TextView
PARAGRAPH= inflater.findViewById(R.id.paragraph) as TextView
}
}
Here's the code to make things work
class MainActivity : AppCompatActivity() {
private var title:TextView? = null
private var paragraph:TextView? = null
override fun onCreate(savedInstanceState :Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
title= inflater.findViewById(R.id.title) as TextView
paragraph= inflater.findViewById(R.id.paragraph) as TextView
}
fun function(v :View) {
val userChoice = "XXX"
val computerChoice = getComputerChoice()
if (userChoice == computerChoice) {
function2(computerChoice)
} else {
title.text = if (computerChoice == "YYY") getString(R.string.YOU_WON) else getString(R.string.YOU_LOSE);
paragraph.text = getString(R.string.STRING, computerChoice)
}
resultScreen()
}
private fun function2(cc :String) {
title.text = getString(R.string.STRING)
paragraph.text = getString(R.string.STRING, cc)
resultScreen()
}
}
If your use case is to show different screens, look at starting more than one Activity and transitioning between them using Intents
What is a good way to do a horizontalLayout in anko / kotlin ? verticalLayout works fine - could set orientation on it but it feels wrong. Not sure what I am missing there.
Just use a linearLayout() function instead.
linearLayout {
button("Some button")
button("Another button")
}
Yeah, LinearLayout is by default horizontal, but I tend to be extra specific and rather use a separate horizontalLayout function for that.
You can simply add the horizontalLayout function to your project:
val HORIZONTAL_LAYOUT_FACTORY = { ctx: Context ->
val view = _LinearLayout(ctx)
view.orientation = LinearLayout.HORIZONTAL
view
}
inline fun ViewManager.horizontalLayout(#StyleRes theme: Int = 0, init: _LinearLayout.() -> Unit): _LinearLayout {
return ankoView(HORIZONTAL_LAYOUT_FACTORY, theme, init)
}
I have opened a feature request at Anko: https://github.com/Kotlin/anko/issues/413