How to set a custom Context in Jetpack Compose? - android

I have run into an issue with Jetpack Compose. In my application's old classes (activity and fragment) I am using a custom Context wrapper for my custom Resources wrapper. This is because string resources values are downloaded from a server; the Strings resource files do contain string IDs, their values are empty and are replaced by the server's values.
In activities and fragments I can simply replace the context in getContext() or attachBaseContext(newBase: Context?) methods, but how do I do this in Jetpack Compose?
Jetpack Compose does have these Context methods: LocalConfiguration.current and LocalContext.current. I love how straightforward this is, but I was not able to find where this Context object is inited. At first, I thought it was in the parent Activity in attachBaseContext() method when I set content, but this code does not seem to be useful:
#AndroidEntryPoint
class MyComposeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
MyComposeActivityScreen()
}
}
}
override fun attachBaseContext(newBase: Context?) {
val language = Locale(Utils.getLanguage(newBase))
super.attachBaseContext(MyCustomContextWrapper.wrap(MyCustomResourcesContextWrapper(newBase), language))
}

You can try specifying the context manually with CompositionLocalProvider:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
updateComposeView(this)
}
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(newBase)
updateComposeView(newBase ?: this)
}
fun updateComposeView(context: Context) {
setContent {
CompositionLocalProvider(
LocalContext provides context
) {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
MyComposeActivityScreen()
}
}
}
}
Also a more Compose way to solve the original problem is to create your own string holder, something like LocalStrings, kind of as shown in this answer.

Related

Show different content (layouts) depending on available width/height in Android Compose

I have an Activity and MyApp composable function. In the function I need to show either list with details or just list screen depending on the available width. How to determine available width for the content I want to show using Jetpack Compose? What is a good practice for this using Compose?
class MyActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ProjectTheme {
MyApp()
}
}
}
}
#Composable
fun MyApp() {
val isLarge: Boolean = ...// how to determine whether the available width is large enough?
if (isLarge) {
ListScreenWithDetails()
} else {
ListScreen()
}
}
#Composable
fun ProjectTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: #Composable () -> Unit) {
// ... project theme
}
You can use :
val configuration = LocalConfiguration.current
Then:
val isLargeWidth = configuration.screenWidthDp > 840
For a generic solution, consider using the Window Size Classes, see this and this for reference.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val windowSizeClass = calculateWindowSizeClass(this)
MyApp(windowSizeClass)
}
}

is it anti-pattern using not top-level fun in jetpack compose?

I'm a beginner to jetpack compose.
Following codelabs, I found they teach me only using top-level function.
I can use the composable function in Activity, but I can't found whoever use this way.
I wanna know which is the best practice.
my code
class RecyclerViewActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp {
Test()
}
}
}
#Composable
fun MyApp(content: #Composable () -> Unit) {
ComposeDemoTheme {
content()
}
}
#Composable
fun Test() {
}
}
codelabs
class RecyclerViewActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp {
Test()
}
}
}
}
#Composable
fun MyApp(content: #Composable () -> Unit) {
ComposeDemoTheme {
content()
}
}
#Composable
fun Test() {
}
You expected to split your code by screens/views, and each screen/view is placed inside it's own file
Usually you don't wanna have any composables in you Activity. You can set theme and other context variables right inside setContent or move it to MyApp if you have too many logic, but in this case I'd stay you move this composable into MyApp.kt too
I usually do structure like this:
screenName: package
Screen.kt
ScreenRow.kt: if screen has a list
ButtomBar.kt: if screen has buttom bar and the code is not trivial
...
So usually when you see that you file contains too many composables you move logically independent parts into other files

Android access view binding val outside onCreate activity

I have an activity that has a button. On the button click I want to update text in text view.
I want to use ViewBinding instead of the normal findViewById
This is how I created the val binding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater);
setContentView(binding.root)
binding.btnRoll.setOnClickListener {
rollDice()
}
}
Now in rollDice I want to update the text view but I'm not able to access binding which make sense because its scope is limited to onCreate() , so what is the best practice for this?
private fun rollDice() {
val random = Random().nextInt(6) + 1
binding.txt_random.setText("random")
}
You have two options.
1. Store in a property
Since the inflated content of Activity is fully bound to it's lifecycle, it's safe to keep the reference as a property
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater);
setContentView(binding.root)
binding.btnRoll.setOnClickListener {
rollDice()
}
}
private fun rollDice() {
val random = Random().nextInt(6) + 1
binding.txt_random.setText("random")
}
}
2. Pass the binding to the methods
That's what I usually do, it avoids creating a property where it's not really a necessity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater);
setContentView(binding.root)
binding.btnRoll.setOnClickListener {
rollDice(binding)
}
}
private fun rollDice(binding: ActivityMainBinding) {
val random = Random().nextInt(6) + 1
binding.txt_random.setText("random")
}
}
Both options are valid ways to make the binding visible to Activities methods.
Store the binding in an instance variable on the Activity. Then you have access to it from all the methods in the Activity.
As the question has accepted answer and it is already addressed but here is my approach to the viewBinding
class MainActivity : AppCompatActivity() {
private val binding by lazy{
ActivityMainBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
binding.btnRoll.setOnClickListener {
rollDice()
}
}
private fun rollDice() {
val random = Random().nextInt(6) + 1
binding.txt_random.setText("random")
}
}
I go with lazy initialization of binding so that way it is only intitialized if it is required.
More you can read about lazy initialization here
https://www.baeldung.com/kotlin/lazy-initialization

How to use interface in Android

I know the concept of "Interfaces" but I've hard time to understand how to use them in android development.
Let's say I created an interface to decide if to show progress bar or not -
interface ProgressBarInterface {
fun showProgressBar()
fun hideProgressBar()
}
And I implement this inside BaseActivity/MainActivity in single Activity app:
class BaseActivity : AppCompatActivity() , ProgressBarInterface {
private val TAG = "MainActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun showProgressBar() {
}
override fun hideProgressBar() {
}
}
And inside my other activity I've a button, that when I click on it, I want to trigger showProgressBar in the base activity:
button.setOnClickListener {
//Show progress bar
}
How can I interact with the interface to trigger the function inside base activity?
Since you already are implementing the interface in your BaseActivity, you can then just add what you need to do inside the interface methods, and then call them up in any point in your activity, if what you are looking for is to extend this BaseActiviy into more activities you will need to make this BaseActivity abstract then you can extend in each activity this BaseClass and just use the interface methods
abstract class BaseActivity : AppCompatActivity() , ProgressBarInterface {
private val TAG = "MainActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun showProgressBar() {
progressBar.visibility = View.VISIBLE
}
override fun hideProgressBar() {
progressBar.visibility = View.GONE
}
}
and then in your Activities you can extend from BaseActivity() and just use your interface methods as you have defined in that BaseActivity() to prevent coding them again, you can do
class FirstActivity : BaseActivity() {
...
button.setOnClickListener {
showProgressBar()
}
An easier way to show and hide the views? Use extension functions
fun View.show() {
this.visibility = View.VISIBLE
}
fun View.hide() {
this.visibility = View.GONE
}
you can define that extensions in any class, for example ViewUtils.kt and then just call
button.setOnClickListener {
progressBar.show()
}
or
button.setOnClickListener {
progressBar.hide()
}

Accessing views from the Activity with Anko

I know I can use an id attribute with Anko to identify a view:
class MainActivityUI : AnkoComponent<MainActivity> {
override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
frameLayout {
textView {
id = R.id.text
}
}
}
}
Then obtain it in the Activity using the find() function (or by using Kotlin Android Extensions):
class MainActivity : AppCompatActivity() {
private val textView by lazy {
find<TextView>(R.id.text)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
MainActivityUI().setContentView(this)
textView.text = "Hello World"
}
}
But I feel like I am missing something; the only place the README mentions the find function or Kotlin Android Extensions is in the section titled Supporting Existing Code:
You don't have to rewrite all your UI with Anko. You can keep your old
classes written in Java. Moreover, if you still want (or have) to
write a Kotlin activity class and inflate an XML layout for some
reason, you can use View properties, which would make things easier:
// Same as findViewById(), simpler to use
val name = find<TextView>(R.id.name)
name.hint = "Enter your name"
name.onClick { /*do something*/ }
You can make your code even more compact by using Kotlin Android
Extensions.
Which makes it seem like the find function is only meant for supporting "old" XML code.
So my question is this; is using an id along with the find function the correct way of accessing a View from the Activity using Anko? Is there a more "Anko" way of handling this? Or am I missing some other benefit of Anko that makes accessing the View from the Activity irrelevant?
And a second related question; if this is the correct way of accessing a View from the Activity, is there a way of creating an id resource (i.e. "#+id/") from within an AnkoComponent? Rather than creating each id in the ids.xml file.
So, why still use XML id to locate the View? since we already use the Anko instead of the XML.
In my opinion, we can store the view elements inside the AnkoComponent instead of the find view's id method. Check the code blow:
class MainActivityUI : AnkoComponent<MainActivity> {
lateinit var txtView: TextView
override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
frameLayout {
txtView = textView {
id = R.id.text // the id here is useless, we can delete this line.
}
}
}
}
class MainActivity : AppCompatActivity() {
lateinit var mainUI : MainActivityUI
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainUI = MainActivityUI()
mainUI.setContentView(this)
mainUI.txtView.text = "Hello World"
}
}
Do not use id to identify views with Anko DSL! It is unnecessary and useless because Anko was designed to get rid off XML layouts. Instead use this pattern:
class ActivityMain : AppCompatActivity() {
var mTextView: TextView // put it here to be accessible everywhere
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ActivityMainUI().setContentView(this)
}
fun yourClassMethod() {
// So this is am example how to get the textView
// defined in your Anko DSL class (not by id!):
mTextView.text = "bla-bla-bla"
}
}
class ActivityMainUI : AnkoComponent<ActivityMain> {
override fun createView(ui: AnkoContext<ActivityMain>) = with(ui) {
// your fancy interface with Anko DSL:
verticalLayout {
owner.mTextView = textView
}
}
}
Please note the UI class definition:
class ActivityMainUI : AnkoComponent<ActivityMain> {
If you put there your activity class name in brackets then all its public variables become accessible via owner in UI class body so you can assing them there.
But you may put AppCompatActivity easily and make some universal class which might be cloned. In this case use lateinit var mTextView :
TextView in the body of UI class as described in Jacob's answer here.
I believe that, as you can add behavior to your Anko files, you don't have to instantiate your views in the activity at all.
That can be really cool, because you can separate the view layer even more. All the code that acts in your views can be inserted in the Anko files. So all you have to do is to call your activity's methods from the Anko and not instantiate any view.
But if you need to instantiate any view... you can use Kotlin Android Extensions in your activity.
Exemple:
Code in your activity:
seekBar.setOnSeekBarChangeListener(object: OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
// Something
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
// Just an empty method
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
// Another empty method
}
})
Code in Anko:
seekBar {
onSeekBarChangeListener {
onProgressChanged { seekBar, progress, fromUser ->
// Something
}
}
}
Now the code is in AnkoComponent. No need to instantiate the view.
Conclusion:
It's a more 'Anko' way to program if you put all your view logic in the AnkoComponents, not in your activities.
Edit:
As an exemple of a code where you don't have to instantiate a view:
In your Anko:
var networkFeedback : TextView = null
override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
frameLayout {
textView {
id = R.id.text2
networkFeedback = this
onClick {
ui.owner.doSomething(2, this)
}
}
}
}
fun networkFeedback(text: String){
networkFeedback.text = text
}
In your activity:
class MainActivity : AppCompatActivity() {
overriding fun onCreate{
[...]
val mainUi = AnkoUi()
// some dynamic task...
mainUi.networkFeedback("lalala")
}
fun doSomething(number: Int, callback: TextView){
//Some network or database task goes here!
//And then, if the operation was successful
callback.text = "Something has changed..."
}
This is a very different approach. I'm not so sure if I like it or not, but this is a whole different discussion...
To generalize the question a bit: How can one make an AnkoComponent that is encapsulated, can be used from the DSL and can have its data programmatically set after creation?
Here is how I did it using the View.tag:
class MyComponent: AnkoComponent<Context> {
lateinit var innerds: TextView
override fun createView(ui: AnkoContext<Context>): View {
val field = with(ui) {
linearLayout {
innerds = complexView("hi")
}
}
field.setTag(this) // store the component in the View
return field
}
fun setData(o:SomeObject) { innerds.setStuff(o.getStuff()) }
}
inline fun ViewManager.myComponent(theme: Int = 0) = myComponent(theme) {}
inline fun ViewManager.myComponent(theme: Int = 0, init: MyComponent.() -> Unit) =
ankoView({ MyComponent(it) }, theme, init)
// And then you can use it anywhere the Anko DSL is used.
class SomeUser : AnkoComponent<Context>
{
lateinit var instance:View
override fun createView(ui: AnkoContext<Context>): View {
linearLayout {
instance = myComponent {}
}
}
fun onDataChange(o:SomeObject) {
(instance.Tag as MyComponent).setData(o)
}
}
}

Categories

Resources