Is it possible to reuse a layout in Kotlin Anko - android

I read that the most benefit of using Anko is its reusability. But i could't find its exact example.
Currently in the new Android layout system, the boiler plate is like below:
DrawerLayout (with some setup)
CoordinatorLayout (with some setup)
AppBarLayout (with some setup)
ToolBar
<The Main Content>
NavigationView (with header inflated)
From the layout structure above, only <The Main Content> is varry. And
in many cases those ceremonial setup duplicated almost in every activity.
So here with Anko im thinking if there is a reusable solution about that issue. Im not expecting it will be reusable for general purpose layout, but et least i can minimize the ceremonial code in the project. Maybe i need something like:
class MainUI: AnkoComponent<MainActivity> {
override fun createView(ui: AnkoContext<MainActivity>): View{
return with(ui) {
myCustomRootLayout {
//here is what <The Main Content> will be
}
}
}
}
From the code above im expecting myCustomRootLayout will do all the ceremonial setup for the root layout such as (DrawerLayout, CoordinatorLayout etc etc).
Is that possible?
EDIT
So i think my question is: How to make a custom component which can host other component

One way to reuse the code is to simply extract myCustomRootLayout into a extension method like so:
class MainUI: AnkoComponent<MainActivity> {
override fun createView(ui: AnkoContext<MainActivity>): View {
return with(ui) {
myCustomRootLayout {
recyclerView()
}
}
}
}
fun <T> AnkoContext<T>.myCustomRootLayout(customize: AnkoContext<T>.() -> Unit = {}): View {
return relativeLayout {
button("Hello")
textView("myFriend")
customize()
}
}
However as stated in the documentation:
Although you can use the DSL directly (in onCreate() or everywhere
else), without creating any extra classes, it is often convenient to
have UI in the separate class. If you use the provided AnkoComponent
interface, you also you get a DSL layout preview feature for free.
It seems to be a good idea to extract the reusable piece into separate AnkoComponent:
class MainUI : AnkoComponent<MainActivity> {
override fun createView(ui: AnkoContext<MainActivity>): View {
return with(ui) {
MyCustomRootLayout<MainActivity>({
recyclerView()
}).createView(ui)
}
}
}
class MyCustomRootLayout<T : Context>(val customize: AnkoContext<T>.() -> Unit = {}) : AnkoComponent<T> {
override fun createView(ui: AnkoContext<T>) = with(ui) {
relativeLayout {
button("Hello")
textView("myFriend")
customize()
}
}
}

I actually found a way to do this, took me a while to figure it out.
I have a very basic test layout here, the content gets added to a
RelativeLayout.
The key here is to add your custom layout in a delegated AnkoContext that delegates to the immediate parent (the RelativeLayout in my case).
abstract class BaseAnkoComponent<T> : AnkoComponent<T> {
companion object {
val TOOLBAR_ID = View.generateViewId()
val COLLAPSING_ID = View.generateViewId()
val COORDINATOR_ID = View.generateViewId()
val APPBAR_ID = View.generateViewId()
val CONTENT_ID = View.generateViewId()
}
abstract fun <T> AnkoContext<T>.content(ui: AnkoContext<T>): View?
override fun createView(ui: AnkoContext<T>) = with(ui) {
coordinatorLayout {
id = COORDINATOR_ID
lparams(matchParent, matchParent)
appBarLayout(R.style.AppTheme_AppBarOverlay) {
id = APPBAR_ID
lparams(matchParent, wrapContent)
fitsSystemWindows = true
collapsingToolbarLayout {
id = COLLAPSING_ID
val collapsingToolbarLayoutParams = AppBarLayout.LayoutParams(matchParent, matchParent)
collapsingToolbarLayoutParams.scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
layoutParams = collapsingToolbarLayoutParams
isTitleEnabled = false
toolbar {
id = TOOLBAR_ID
val toolbarLayoutParams = CollapsingToolbarLayout.LayoutParams(matchParent, dimenAttr(R.attr.actionBarSize))
toolbarLayoutParams.collapseMode = CollapsingToolbarLayout.LayoutParams.COLLAPSE_MODE_PIN
layoutParams = toolbarLayoutParams
minimumHeight = dimenAttr(R.attr.actionBarSize)
background = ColorDrawable(colorAttr(R.attr.colorPrimary))
popupTheme = R.style.AppTheme_PopupOverlay
}
}
}
with(AnkoContext.createDelegate(relativeLayout {
id = CONTENT_ID
val relativeLayoutParams = CoordinatorLayout.LayoutParams(matchParent, matchParent)
relativeLayoutParams.behavior = AppBarLayout.ScrollingViewBehavior()
layoutParams = relativeLayoutParams
})) {
content(ui)
}
}
}
}
And then you can extend the BaseAnkoComponent and build your content in the same way with Anko DSL.
class FooActivityUi : BaseAnkoComponent<FooActivity>() {
override fun <T> AnkoContext<T>.content(): View? {
return verticalLayout {
lparams(width = matchParent, height = matchParent)
button("Hello")
textView("myFriend")
}
}
}
I am sure there is a better way to do this but I have not found it. Kinda new to Kotlin and Anko.

Related

Android Espresso can't find a Edittext inside a custom view class

Possible Duplicate 1
Possible Duplicate 2
I have a custom View which contains textview and edittext. I want to access the Edittext in my custom View for UI test. But I dont want to use custom ViewAction to setEdittext because in that case I wont be able to support the methods like typeText. Here is my test method
#RunWith(AndroidJUnit4::class)
class LoginFragmentTest : BaseTest() {
#Test
#Throws(InterruptedException::class)
fun testLoginForm() {
val navController = TestNavHostController(ApplicationProvider.getApplicationContext())
val loginScenario = launchFragmentInContainer<LoginFragment>()
loginScenario.onFragment { fragment ->
navController.setGraph(R.navigation.nav_login)
Navigation.setViewNavController(fragment.requireView(), navController)
}
onView(allOf(withId(R.id.etForm), isDescendantOfA(withId(R.id.email)))).perform(typeText("user#email.com"))
onView(allOf(withId(R.id.etForm), isDescendantOfA(withId(R.id.password)))).perform(typeText("123456"))
onView(withId(R.id.login)).perform(click())
}
}
I am getting following error
androidx.test.espresso.NoMatchingViewException: No views in hierarchy found matching: (with id is <com.example.cicddemo:id/etForm> and is descendant of a: with id is <2131296433>)
Note: I have implemented it by custom ViewAction which is working fine. But I am not able to get typeText functionality.
ViewonView(withId(R.id.email)).perform(setTextEditText(newText = "user#email.com"))
onView(withId(R.id.password)).perform(setTextEditText(newText = "123456"))
Custom ViewAction:
fun setTextEditText(
newText: String ?
): ViewAction {
return object: ViewAction {
override fun getConstraints(): Matcher < View > {
return CoreMatchers.allOf(
ViewMatchers.isDisplayed(),
ViewMatchers.isAssignableFrom(FormView::class.java)
)
}
override fun getDescription(): String {
return "Update the text from the custom EditText"
}
override fun perform(uiController: UiController ? , view : View) {
(view as FormView).setText(newText)
}
}
}
Is it possible to access the actualy edittext inside the Custom view class and pass it for test?
I have solved my problem by retrieving views by tags without using any custom ViewActions. The issue was that in my custom view I was using
etForm.id = View.generateViewId()
As id was changing at runtime so I am setting tags and access by
withTagValue
Here is updated code
#Test
#Throws(InterruptedException::class)
fun testInvalidEmailPassword() {
val emailViewInteraction = onView(allOf(withTagValue(`is`("email" as Any?)), isDescendantOfA(withId(R.id.email))))
val passwordViewInteraction = onView(allOf(withTagValue(`is`("password" as Any?)), isDescendantOfA(withId(R.id.password))))
emailViewInteraction.perform(typeText("user#email.com"))
passwordViewInteraction.perform(typeText("123456"))
onView(withId(R.id.login)).perform(click())
}
If this is AWS FormView, just match the LinearLayout, which it extends
...else you might eventually need to write a custom view-matcher.

How to group a bunch of views and change their visibility together

I have an activity which contains 2 group of views, which CAN'T be located into same LAYOUT group but belong to same LOGIC group, meaning that they should be shown or hidden and bind click event at same time. The thing is that I feel really awful to write something like this:
fun hide() {
view1.visibility = View.GONE
view2.visibility = View.GONE
view3.visibility = View.GONE
// ...
view9.visibility = View.GONE
}
fun show() {
view1.visibility = View.VISIBLE
view2.visibility = View.VISIBLE
view3.visibility = View.VISIBLE
// ...
view9.visibility = View.VISIBLE
view1.setOnClickListener{ run() }
view2.setOnClickListener{ run() }
view3.setOnClickListener{ run() }
// ...
view9.setOnClickListener{ run() }
}
I did read a post which describes a kotlin skill to simplify this mess by somehow grouping those views then just handle the groups, but unfortunately I can no longer find that post..
Help would be appreciated!
========= Update 2019-07-31 =========
I found the solution but forgot to update this question, the 'grouping' I was looking for, is in fact not a Kotlin specific feature but simply using vararg, and we can use Kotlin extension (which is AWESOME) to simplify a bit more:
// assume we have a base activity or fragment, then put below functions in there
fun View.show() {
visibility = View.VISIBLE
}
fun show(vararg views: View) {
views.forEach { it.show() }
}
fun View.hide() {
visibility = View.GONE
}
fun hide(vararg views: View) {
views.forEach { it.hide() }
}
// then in any activity or fragment
show(v1, v2, v3, v4)
v9.hide()
============= updated 2020-03-07 ================
This is exactly androidx.constraintlayout.widget.Group designed to do, which can logically group a bunch of views from anywhere and control their visibility by only changing group's visibility.
Since ConstraintLayout 1.1 you can use Group instead of LayoutGroup.
You can simply add this code to you XML layout
<android.support.constraint.Group
android:id="#+id/profile"
app:constraint_referenced_ids="profile_name,profile_image" />
And then you can call it from code to achieve behavior, that you need
profile.visibility = GONE
profile.visibility = VISIBLE
For more details read this article https://medium.com/androiddevelopers/introducing-constraint-layout-1-1-d07fc02406bc
You need to create extension functions.
For example:
fun View.showGroupViews(vararg view: View) {
view.forEach {
it.show()
}
}
fun View.hideGroupViews(vararg view: View) {
view.forEach {
it.hide()
}
}
Create a list of views and loop on it
val views = listOf<View>(view1, view2, ...)
views.forEach {
it.visibility = View.GONE
}
You can also create extension function of Iterable<View> to simplify any kind of action on listed views
fun Iterable<View>.visibility(visibility: Int) = this.forEach {
it.visibility = visibility
}
//usage
views.visibility(View.GONE)
Maybe you want to locate all views from tags in XML. Take a look at this answer
Depending on how is your layout structured you might want to group those views in a ViewGroup like LinearLayout, RelativeLayout, FrameLayout or ConstraintLayout.
Then you can change visibility just on the ViewGroup and all of its children will change it too.
Edit:
Without ViewGroup the only solution to eliminating this boilerplate is to enable databinding in your project and set it like this:
In your Activity/Fragment:
val groupVisible = ObservableBoolean()
fun changeVisibility(show: Boolean) {
groupVisible.set(show)
}
In your xml:
<layout>
<data>
<variable name="groupVisible" type="Boolean"/>
</data>
<View
android:visibility="#{groupVisible ? View.VISIBLE : View.GONE}"/>
</layout>
Why don't you create an array:
val views = arrayOf(view1, view2, view3, view4, view5, view6, view7, view8, view9)
then:
fun show() {
views.forEach {
it.visibility = View.VISIBLE
it.setOnClickListener{ }
}
}
fun hide() {
views.forEach { it.visibility = View.INVISIBLE }
}
Or without an array if the names of the views are surely like view1, view2, ...
for (i in 1..9) {
val id = resources.getIdentifier("view$i", "id", context.getPackageName())
val view = findViewById<View>(id)
view.visibility = View.VISIBLE
view.setOnClickListener{ }
}
You can define a function with three parameters and use vararg like following code:
fun changeVisiblityAndAddClickListeners(visibility: Int,
listener: View.OnClickListener,
vararg views: View) {
for (view: View in views) {
view.visibility = visibility
if (visibility == View.VISIBLE) {
view.setOnClickListener(listener)
}
}
}
Of course if you have too many views, this is not a effective solution. I just added this code snippet for an alternative way especially for problems with dynamic view set.
If your views are not inside a view group you can use an extension function. You could create one to toggle the visibility of the views:
fun View.toggleVisibility() {
if (visibility == View.VISIBLE) {
visibility = View.GONE
} else {
visibility = View.VISIBLE
}
}
And you can use it like this:
view.toggleVisibility()
First in your xml layout, group your views by android:tag="group_1" attribute.
Then inside your activity use a for loop to implement whatever logic you need:
val root: ViewGroup = TODO("find your root layout")
for (i in 0 until root.childCount) {
val v = root.getChildAt(i)
when (v.tag) {
"group_1" -> {
TODO()
}
"group_2" -> {
TODO()
}
else -> {
TODO()
}
}
}
You can create a LinearLayout or any other ViewGroup containing your child Views and give it an ID in the XML file, then in your Activity or Fragment class define these functions:
fun disableViewGroup(viewGroup: ViewGroup) {
viewGroup.children.iterator().forEach {
it.isEnabled = false
}
}
fun enableViewGroup(viewGroup: ViewGroup) {
viewGroup.children.iterator().forEach {
it.isEnabled = true
}
}
And then in onCreate() or onStart() call it as following:
disableViewGroup(idOfViewGroup)
The children method returns a Sequence of children Views in the ViewGroup which you can iterate by forEach and apply whatever operation applicable to Views.
Hope it helps!

How can I redraw an Anko frameLayout when my model changes?

I have an Anko component where I create my view with some code like this:
override fun createView(ui: AnkoContext<T>) = with(ui) {
frameLayout {
var imgView = imageView(R.drawable.ic_1).lparams {
horizontalMargin = ...
topMargin = ...
width = ...
height = ...
}
imgView.backgroundColor = gameModel.colour
}
}
The background of my imgView depends on a colour in my model.
Let's imagine I update my model elsewhere. How do I 'refresh' the Anko component UI to reflect the new gameModel.colour? I've never done any Android before but it seems one would usually use either invalidate() or requestLayout() but they don't seem to work.

Anko view from class

I have implemented a class which does various api-request, my idea was that every instance of the class has a method to create a view to have tile like interface.
My problem is i don't know how this should be implemented in a good way.
What is the prefered way doing this using Anko and Kotlin?
Anko has great documentation about that case (But who read docs, yeah?)
Let's say, CustomView is your custom View class name, and
customView is what you want to write in the DSL.
If you only plan to use your custom View in the DSL surrounded by
some other View:
inline fun ViewManager.customView(theme: Int = 0) = customView(theme) {}
inline fun ViewManager.customView(theme: Int = 0, init: CustomView.() -> Unit) = ankoView({ CustomView(it) }, theme, init)
So now you can write this:
frameLayout {
customView()
}
…or this (see the UI wrapper chapter):
UI {
customView()
}
But if you want to use your view as a top-level widget without a UI
wrapper inside Activity, add this as well:
inline fun Activity.customView(theme: Int = 0) = customView(theme) {}
inline fun Activity.customView(theme: Int = 0, init: CustomView.() -> Unit) = ankoView({ CustomView(it) }, theme, init)
Example (that's just how I would use it, you may choose different approach):
class YourAwesomeButton: Button() {
/* ... */
fun makeThisButtonAwesome() {/* ... */}
}
/** This lines may be in any file of the project, but better to put them right under the button class */
inline fun ViewManager.yourAwesomeButton(theme: Int = 0) = yourAwesomeButton(theme) {}
inline fun ViewManager.yourAwesomeButton(theme: Int = 0, init: CustomView.() -> Unit) =
ankoView({ YourAwesomeButton(it) }, theme, init)
In another file:
class YourAwesomeActivity: Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(saveInstanceState)
relativeLayout(R.style.YourAwesomeAppTheme) {
yourAwesomeButton(R.style.YourAwesomeAppTheme) {
makeThisButtonAwesome()
}.lparams {
centerInParent()
}
}
}
}

Horizontal LinearLayout in Anko

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

Categories

Resources