I am working with overload functions in Kotlin.
In this schematic example, suppose a function whose only difference is the type of view that I pass to the function. One uses TextView, the other uses Button, so I have 2 different functions.
fun workWithViews(v:TextView,...){
// code
}
fun workWithViews(v:Button,...){
// same code!
}
In this case, the properties I use are the same (isAllCaps, gravity, etc.). The problem is that I have to place the same code twice, i.e., the whole code is exactly the same.
It happens because isAllCaps (just like many other properties) it is not a general property of a view, but of some types of views
So it doesn't work, because obviously the compiler see the function parameter, not the real parameter.
I also can make function with a view type parameter, with a big when with my type possibilities:
fun workWithTextView(v:View,...){
when{
(v is TextView) -> {
// code
}
(v is Button) {
// same code
}
} // when
}
The 2 solutions are terrible and generate duplicate code or boilerplate.
I can also do the when before each access to some field, which makes things even worse. Now imagine if one has 5 similar types instead of 2, with many fields in common.
I read some suggestions to create union types in Kotlin. It would be great!
For instance:
fun workWithViews(v:(TextView, Button),...){
// just one code repetition....
}
or
union textBut = TextView , Button
fun workWithViews(v:textBut ,...){
// just one code repetition....
}
In that case I would only have to test a certain type (if (v is typeX)) if I used something specific for that type.
Is there some best solution?
Button is a subclass of TextView, so you can make the function signature take a TextView and put Button-specific stuff in an if-block.
fun workWithTextView(textView: TextView) {
// Do stuff common to TextViews and Buttons.
if (textView is Button) {
// Do extra stuff only for Buttons.
}
}
If Views have common methods, then most likely one view extends the other.
For example, Button extends TextView and you can do this:
fun workWithViews(v:TextView){
// TextView code
}
fun workWithViews(v:Button){
// Button specific code
workWithViews(v as TextView)
}
Related
This answer demonstrates how to embed a link within an annotated string and make it clickable. This works great and triggers the on click with the correct URL. However, I can't seem to write a test that clicks the annotated text to open the link. Has anyone had success writing a test like this? My production code is very similar to what is in the answer. Below is my test code:
#Test
fun it_should_open_terms_of_service_link() {
val termsOfServiceText = getString(R.string.settings_terms)
try {
Intents.init()
stubAnyIntent()
composeTestRule.onNode(hasText(termsOfServiceText, substring = true)).performClick()
assertLinkWasOpened(getString(R.string.settings_terms_link))
} finally {
Intents.release()
}
}
It looks like hasText(termsOfServiceText, substring = true) fetches the entire annotated string node opposed to just the substring, "Terms of Service". Thus, the on click method does get triggered, just not at the correct position in the annotated string. Happy to provide more info if needed. Thanks!
You can perform the click at an offset. This performs the click at the tail end horizontally, and in the middle vertically.
composeTestRule.onNode(hasText(termsOfServiceText, substring = true))
.performGesture { click(percentOffset(.9f, .5f)) }
The disadvantage is that it makes the test more fragile.
Background
I'm currently investigating options for creating layouts, during the development of the project I'm looking to possibly migrate the UI to Jetpack Compose or post release, depending on the stability/flexibility of the library.
Part of the project will be using a server driven UI. However the twist being the UI is not known ahead of time and will be dynamic (server and data driven).
I have no issues with handling the business logic and presentation layers however when it comes to the UI I will need a requirement to dynamically build the UI based on the presentation data and view models.
TL;DR
With this in mind is it possible to create dynamic layouts (not to be confused with dynamic layout data) using Jetpack Compose?
As a minimal example, with traditional View and ViewGroup this can easily be achieved :
class DynamicViewActivity : AppCompatActivity() {
private lateinit var root : LinearLayout
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
// setup view group container
root = LinearLayout(this)
root.orientation = LinearLayout.VERTICAL
root.layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT)
setContentView(root, LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT))
// some lookup to create a dynamic layout
val children : List<Pair<View, LinearLayout.LayoutParams>> = getChildren(someArgs)
// add child views
children.forEach { (view, params) -> root.addView(view, params) }
}
fun <T : View> addViewToRoot(view: T, params: LinearLayout.LayoutParams) {
root.addView(view, params)
}
fun removeFromRoot(viewTag : String) {
root.findViewWithTag<View>(viewTag)?.let(root::removeView)
}
}
How do you do the same with Jetpack Compose?
Update
Following the answer from #CommonsWare I implemented the UI in Compose. As my actual code has a very thin UI layer, with all listeners and events using one and two-way data binding, and "unknowns" in the answer already addressed in my project it was incredibly easy to just swap the UI over.
Having said that I soon realised that simple things like ScrollView and View::tooltipText do not yet exist in Compose. Also there is no easy way to have layouts based on runtime configuration (screen orientation / screen bucket size etc) in comparison to xml layouts/resources. This means for me, using data binding with all the rich View framework and libraries is still the better solution.
Looking forward to Compose library updates and maybe look at some point in the future.
With this in mind is it possible to create dynamic layouts (not to be confused with dynamic layout data) using Jetpack Compose?
Sure. Compose is all functions. You can parse data and call functions based on that data, whether that data is "fill in this pre-defined UI structure" or that data is "define the UI structure".
For example, suppose your server has an endpoint that returns the following JSON:
[
{
"element": "label",
"attributes": {
// values omitted for brevity
}
},
{
"element": "field",
"attributes": {
// values omitted for brevity
}
},
// additional elements omitted for brevity
]
Your job is to assemble a UI based on that JSON. A label element should be fixed text, a field element should be a text entry field, and so on for various types. The attributes object contains details that vary by element.
So, you parse that. Suppose that you wind up with a List<UiElement> as a result, where UiElement is an interface or abstract class or something, with sub-types based upon the supported elements (e.g., LabelElement, FieldElement). Now your job is to construct a UI based on that List<UiElement>.
In View-space, you could have a function that creates a View based on a supplied UiElement:
fun buildView(element: UiElement) = when (element) {
is LabelElement -> buildTextView(element)
is FieldElement -> buildEditText(element)
else -> TODO("add other element cases here")
}
buildTextView() would assemble a TextView, whether inflating from a layout or calling a constructor. buildEditText() would assemble an EditText, whether inflating from a layout or calling a constructor. And so on. Each of those functions would be responsible for grabbing values out of the attributes and doing something useful with them, such as setting the text in a TextView or the hint in an EditText.
In the code snippet in your question, rather than your getChildren()-and-loop approach, you would iterate over the List<UiElement> and call buildView() for each UiElement in the list, and adding the result to your LinearLayout.
The Compose equivalent would be something like this:
#Composable
fun buildNode(element: UiElement) {
when (element) {
is LabelElement -> buildTextNode(element)
is FieldElement -> buildTextFieldNode(element)
else -> TODO("add other element cases here")
}
}
IOW, it would be nearly identical. The key differences are:
the #Composable annotation (also required on buildTextNode() and buildTextFieldNode())
no need to return anything, as composables are added automatically to the parent
the details of what goes in buildTextNode() and buildTextFieldNode() would be reminiscent of buildTextView() and buildEditText(), but based on composables
Your activity would have something like this:
Column {
uiElements.forEach { buildNode(it) }
}
...as a replacement for your LinearLayout.
(in reality, both examples would need a scrolling container, but we'll ignore that here as well)
All of the complexity of server-defined UI lies outside of the scope of your code sample:
How do I parse the server response?
How do I map that server response into an object model representing the desired UI?
How do I get my per-element UI bits to work?
How do I handle event listeners?
More generally, what are we doing in response to user input on this UI?
How will we re-generate this UI as needed (for views, based on configuration changes; for composables, based on recomposition)?
And so on
Some of that will be identical between a View-based UI and a Compose-based UI — JSON parsing, for example. Some of that will be substantially different, such as handling user input.
But the general approach of "parse the server response and create UI elements based on that response", views and composables are equally up to the challenge. In particular, at the level of the code sample in your question, views and composables can both handle your high-level scenario. The devil is in the details.
Let's assume i' m programming an app showing some kind of countdown.
// somewhere in my fragment:
fun getCountdown(): LiveData<Int> = viewModel.countdown
// 10 ... 9 ... 8 ... etc.
I now want to bind this LiveData to two different TextViews.
<TextView
android:id="#+id/countdownTextView"
android:text="#{fragment.getCountdown}" />
<TextView
android:id="#+id/hurryUpTextView"
android:text="#{fragment.getCountdown}" />
If i only had one of those two views, my BindingAdapter(s) would look like this:
// for the countdown-TextView:
#BindingAdapter("android:text")
fun bindCountdownToTextView(view: TextView, state: LiveData<LoggedInSubState>) {
view.setText("$ Seconds remaining!")
}
// OR for the hurry-up-TextView:
#BindingAdapter("android:text")
fun bindCountdownToTextView(view: TextView, state: LiveData<LoggedInSubState>) {
if(state.value < 3){
view.setText("Hurry up!")
} else {
view.setText("Chill, you have a lot of time")
}
}
Now, what's the most elegant way to use both TextViews/Adapters together?
Should i just create two LiveDatas in my fragment which map countdown to the appropriate string?
Can i somehow specify which adapter i'd like to use?
Should i (try to) write one adapter which internally differenciates between the two views?
Better suggestions?
To make code simple and readable, I'll advise you choose between next approaches:
Use one binding adapter and supply to it already prepared data - your logic particulary will be placed in View or ViewModel.
Use different binding adapter names and post raw data - all work will be placed in each adapter.
What is best suited for you depends on your needs, how common is format logic and , of course, personal opinion.
I am developing an Android application using Kotlin programming language. I am starting to use the Model-View-Presenter pattern for my application. To be honest, this is my first time using the MVP pattern even though I have been doing Android Development quite a little while. Now, I am a little bit struggling following the best practices and standards of the pattern.
This is the feature now I am currently developing. A very simple and basic feature.
The application will update a text view. When the text view is updated, a string (message) will be passed to a function as a parameter. Inside the function, if the string is empty, it will hide the text view. Otherwise, it will show the text view again. My struggle is that I am not quite sure how to implement my code that fits the MVP pattern. Especially, the logic to toggle the visibility state of the text view.
This is the current signature of my presenter class called, LoginActivityPresenter
class LoginActivityPresenter(viewArg: View)
{
private var view:View = viewArg
interface View
{
fun updateErrorMessageTextView(message: String)
}
}
What I am thinking is that the activity class will implement the LoginActivityPresenter.View and provide the implementation of the updateErrorMessageTextView method. So in the activity, inside one single function, it will do the following tasks.
It will check if the string is empty. If yes, it will hide the view. Otherwise, it will show the view (back). Then it will set the text view with the message. All in the activity class because of the above presenter class signature.
The other way I am thinking of implementing LoginActivityPresenter class is as follows.
class LoginActivityPresenter(viewArg: View)
{
private var view:View = viewArg
fun updateErrorMessage(message: String) {
if (message.isNullOrEmpty()) {
this.view.hideErrorTextView()
return
}
this.view.showErrorTextView()
this.view.updateErrorMessageTextView(message)
}
interface View
{
fun updateErrorMessageTextView(message: String)
fun showErrorTextView()
fun hideErrorTextView()
}
}
So in the second implementation, the presenter class will handle the logic of showing or hiding the view based on the string value rather than logic is being handled by the activity class.
My question is that which approach is more suitable and standard way for the MVP pattern and why?
I am building an app on MVVM+Kotlin+Databinding, and i have this situation i am stuck at.
I have LoginFragment which has a phone number edittext and a button,
Now i need to check if the phone number is empty or not when user clicks the button.
Normally i would do that by using this code in my fragment.
if(!binding!!.phone.text.isEmpty()) {
//do something
}
But according to experts my view should not know anything about the business logic, Hence i need to have this check inside my viewModel.
so what should be the best way to achieve this?
Here is the bet practice to achieve that (from my point of view):
In your layout add text watcher and text to your EditText
android:text="#{view_model.phone}"
app:addTextChangedListener="#{view_model.phoneWatcher}"
and on click method to your button
android:onClick="#{() -> view_model.save()}"
Inside the ViewModel you will have text observable and a watcher
val phone = ObservableField<String?>()
val phoneWatcher = object : TextWatcherAdapter() {
override fun afterTextChanged(s: Editable?) {
phone.set(s?.toString())
}
}
Now you can make your check inside ViewModel
fun save() {
if (phone.get()?.isNotEmpty == true) {
// TODO: do something
}
}
Also please note that it is a best practice to avoid doing something like that binding!!.phone in Kotlin. If you're using !! to make a possible nullable object look like it is not-nullable (even if you're 100% sure it is) - you're doing something wrong.