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()
}
}
}
}
Related
How do I have multiple callback function back to the Activity/Fragment of a RecyclerView?
I have multiple options for each item in the RecyclerView (Edit, Delete, CheckedAsComplete, View) and I would like to have callback function for each of them in the Activity/Fragment of the RecyclerView.
Here is a link of how I got one callback in the Adapter: https://www.geeksforgeeks.org/kotlin-lambda-functions-for-recyclerview-adapter-callbacks-in-android/
I just need to know if it is possible to have multiple callbacks in the adapter and if so, how do I implement it?
My Activity's Adapter Code:
val adapter = ProductAdapter(this) {
deleteProduct(it),
editProduct(it),
viewProduct(it),
checkAsComplete(it)
}
Here is my Adapter's Constructor:
class ProductAdapter(
private var context: Context,
private val deleteProduct: (ItemTable) -> Unit,
private val editProduct: (ItemTable) -> Unit,
private val viewProduct: (ItemTable) -> Unit,
private val checkedAsComplete: (ItemTable) -> Unit
): RecyclerView.Adapter<ProductAdapter.ItemProductViewHolder>() {
// Rest of RecyclerView Adapter Code
}
I'm pretty new to kotlin so I would really appreciate your help!
You can use curly braces out only for the last callback of the list.
Assuming you declared the following methods in your activity :
fun deleteProduct(itemTable: ItemTable)
fun editProduct(itemTable: ItemTable)
fun checkAsComplete(itemTable: ItemTable)
fun viewProduct(itemTable: ItemTable)
You can use named parameters and you have two choices
With method reference
val adapter = ProductAdapter(
context = this,
deleteProduct = ::deleteProduct,
editProduct = ::editProduct,
viewProduct = ::viewProduct,
checkAsComplete = ::checkAsComplete
)
With lambda
val adapter = ProductAdapter(
context = this,
deleteProduct = { deleteProduct(it) },
editProduct = { editProduct(it) },
viewProduct = { viewProduct(it) },
checkAsComplete = { checkAsComplete(it) }
)
You can use different approach. This does not depend on how many events you have. For example with enum class you can use single callback with many options
class ProductAdapter(private val clickEvent: (ClickEvent, ItemTable) -> Unit):
RecyclerView.Adapter<ProductAdapter.ItemProductViewHolder>() {
enum class ClickEvent {
DELETE,
EDIT,
VIEW,
COMPLETE
}
}
Usage:
val adapter = ProductAdapter{ event, item ->
when(event){
DELETE -> deleteProduct(item)
....//All other enum values
}
}
I am using data binding and having trouble solving multiple quick click. I do not want to put a logic in every click instead, I want to create a solution once and expect it to work throughout my project.
I found one solution here. Code snippet from that page is as follows:
class SafeClickListener(
private var defaultInterval: Int = 1000,
private val onSafeCLick: (View) -> Unit
) : View.OnClickListener {
private var lastTimeClicked: Long = 0
override fun onClick(v: View) {
if (SystemClock.elapsedRealtime() - lastTimeClicked < defaultInterval) {
return
}
lastTimeClicked = SystemClock.elapsedRealtime()
onSafeCLick(v)
}
}
And using extension funciton:
fun View.setSafeOnClickListener(onSafeClick: (View) -> Unit) {
val safeClickListener = SafeClickListener {
onSafeClick(it)
}
setOnClickListener(safeClickListener)
}
Now, for any view we can simply call:
anyView.setSafeOnClickListener {
doYourStuff()
}
Which is awesome. But it only applies if I am calling setOnClickListener to a view but I am using data binding. Where I am using something like this:
android: onClick="#{(view) -> myViewModel.UIEvents(SomeUIEvent.showDialog)}"
I am aware that if I can create a binding adapter, I would be able to solve the problem. But, I couldn't make one that works.
How can I achieve something that I can use with data binding and that works globally like the above example?
Thanks
maybe you can use #BindingAdapter.
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.
I am rendering a form based on JSON response that I fetch from the server.
My use case involves listening to a click from a radio button, toggling the visibility of certain text fields based on the radioButton selection, and refreshing the layout with the visible textView.
The expected output should be to update the same view with the textView now visible, but I'm now seeing the same form twice, first with default state, and second with updated state.
Have I somehow created an entirely new model_ class and passing it to the controller? I just want to change the boolean field of the existing model and update the view.
My Model Class
#EpoxyModelClass(layout = R.layout.layout_panel_input)
abstract class PanelInputModel(
#EpoxyAttribute var panelInput: PanelInput,
#EpoxyAttribute var isVisible: Boolean,
#EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var context: Context,
#EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var textChangedListener: InputTextChangedListener,
#EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var radioButtonSelectedListener: RadioButtonSelectedListener,
#EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var validationChangedListener: ValidationChangedListener
) : EpoxyModelWithHolder<PanelInputModel.PanelInputHolder>() {
#EpoxyAttribute var imageList = mutableListOf<ImageInput>()
override fun bind(holder: PanelInputHolder) {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
generateViews(holder, inflater, panelInput.elements) // Generates textViews, radioButtons, etc, based on ElementType enum inside Panel input
}
fun generateRadioButtonView(element: Element) {
// Created a custom listener and calling its function
radioButtonSelectedListener.radioButtonSelected(chip.id, chip.text.toString())
}
fun generateTextView() {
// Show/hide textView based on isVisible value
}
My Controller Class
class FormInputController(
var context: Context,
var position: Int, // Fragment Position in PagerAdapter
var textChangedListener: InputTextChangedListener,
var radioButtonSelectedListener: RadioButtonSelectedListener,
var validationChangedListener: ValidationChangedListener
) : TypedEpoxyController<FormInput>() {
override fun buildModels(data: FormInput?) {
val panelInputModel = PanelInputModel_(
data as PanelInput,
data.isVisible,
context,
textChangedListener,
radioButtonSelectedListener,
validationChangedListener
)
panelInputModel.id(position)
panelInputModel.addTo(this)
}
}
My fragment implements the on radio button checked listener, modifies the formInput.isVisible = true and calls formInputController.setData(componentList)
Please help me out on this, thanks!
I don't think you are using Epoxy correctly, that's not how it's supposed to be.
First of all, let's start with the Holder: you should not inflate the view inside of bind/unbind, just set your views there. Also, the view is inflated for you from the layout file you are specifying at R.layout.layout_panel_input, so there is no need to inflate at all.
You should copy this into your project:
https://github.com/airbnb/epoxy/blob/master/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/helpers/KotlinEpoxyHolder.kt
And create your holder in this way:
class PanelInputHolder : KotlinHolder() {
val textView by bind<TextView>(R.id.your_text_view_id)
val button by bind<Button>(R.id.your_button_id)
}
Let's move to your model class: you should remove these variables from the constructor as they are going to be a reference for the annotation processor to create the actual class.
Also, don't set your layout res from the annotation as that will not be allowed in the future.
Like so:
#EpoxyModelClass
class PanelInputModel : EpoxyModelWithHolder<PanelInputHolder>() {
#EpoxyAttribute
lateinit var text: String
#EpoxyAttribute(DoNotHash)
lateinit var listener: View.OnClickListener
override fun getDefaultLayout(): Int {
return R.layout.layout_panel_input
}
override fun bind(holder: PanelInputHolder) {
// here set your views
holder.textView.text = text
holder.textView.setOnClickListener(listener)
}
override fun unbind(holder: PanelInputHolder) {
// here unset your views
holder.textView.text = null
holder.textView.setOnClickListener(null)
}
}
Loop your data inside the controller not inside the model:
class FormInputController : TypedEpoxyController<FormInput>() {
override fun buildModels(data: FormInput?) {
data?.let {
// do your layout as you want, with the models you specify
// for example a header
PanelInputModel_()
.id(it.id)
.text("Hello WOrld!")
.listener { // do something here }
.addTo(this)
// generate a model per item
it.images.forEach {
ImageModel_()
.id(it.imageId)
.image(it)
.addTo(this)
}
}
}
}
When choosing your id, keep in mind that Epoxy will keep track of those and update if the attrs change, so don't use a position, but a unique id that will not get duplicated.
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.