-
Notifications
You must be signed in to change notification settings - Fork 726
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How do I use Epoxy to create forms? #426
Comments
The quick answer is that you need to register a text watcher on the model and save the text value to a field whenever it changes. Rebuilding models should rely on that field to set the data. Models should also be rebuilt whenever the field is updated, but it is recommend to use Since models need to be rebuilt there is a surprisingly tricky issue with the cursor maintaining the correct position. Our app has a lot of pages with forms that we use Epoxy with, and it took us a while to get it right, but I created some utils to help and we have a pretty good pattern now. I am leaving on a trip today, but I will try to share sample code and more details soon. It's something I've been meaning to do for a while because it isn't too straightforward and has some gotchas to it. #218 is slightly related and you could read through that for some details |
Hi @elihart .. we really need those utils 😄 We want to build something like Eureka (a library for iOS) but for Android. We would appreciate your help. |
@yesez5 Sorry for the delay, things have been busy. That's great that you want to work on an Android version of Eureka though! Here is the pattern we use, it's pretty straightforward. This doesn't include the part of the code that handles the TextWatcher and callback to rebuild models, if you are confused about that part let me know and I can provide another snipped. I still mean to provide an official wiki entry and sample for this, but hopefully I can unblock you in the meantime @TextProp
public void setInputText(@Nullable CharSequence text) {
if (setText(editText, text)) {
// If the text changed then we move the cursor to the end of the new text. This allows us to fill in text programmatically if needed,
// like a search suggestion, but if the user is typing and the view is rebound we won't lose their cursor position.
editText.setSelection(editText.length());
}
}
public static boolean setText(TextView textView, @Nullable CharSequence text) {
if (!isTextDifferent(text, textView.getText())) {
// Previous text is the same. No op
return false;
}
textView.setText(text);
return true;
}
/**
* @return True if str1 is different from str2.
* <p>
* This is adapted from how the Android DataBinding library binds its text views.
*/
public static boolean isTextDifferent(@Nullable CharSequence str1, @Nullable CharSequence str2) {
if (str1 == str2) {
return false;
}
if ((str1 == null) || (str2 == null)) {
return true;
}
final int length = str1.length();
if (length != str2.length()) {
return true;
}
if (str1 instanceof Spanned) {
return !str1.equals(str2);
}
for (int i = 0; i < length; i++) {
if (str1.charAt(i) != str2.charAt(i)) {
return true;
}
}
return false;
} |
Hi @elihart, What is the best way to handles the TextWatcher and callback to rebuild models? |
My Approach is:
|
please add an sample for it . |
Do we have a sample in the Wiki for this as Eli mentioned? Thanks. |
No, this hasn't been done yet. I'll update this issue when it is. If anyone wants to help that would be great |
Having just hit this and followed the advice here, I'm left wondering why this is so hard. Using the This sort of leads me to wonder if rebuilding the models is actually a good idea for certain state changes? Introducing a delay, via I'm currently using DataBinding in combination with Epoxy and I guess two-way DataBinding isn't a thing in Epoxy (yet). Much of the advice here seems to apply to something other than DataBinding anyway, so what I've come up with might not be following the advice above completely. |
hi guys, |
@sandys this doesn't apply to photos, it only applies to EditText. Photos should be handled like any other type of data. @nealsanche As long as the delay is short enough so it isn't feasible for the model's view to be scrolled off and back on then there won't be issues. As far as the cursor jumping goes, if you use the sample code above then
If you are seeing the cursor jump, I would debug your code in |
We would really appreciate a dedicated wiki article on the EditText handling. |
+1 on this. really need this. even if you dont do an article, please put a
sample screen/testcase. that's sufficient.
…On Sat, Sep 28, 2019 at 1:10 AM zirkler ***@***.***> wrote:
We would really appreciate a dedicated wiki article on the EditText
handling.
Or maybe share with us how you are handling at in the Airbnb App.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#426?email_source=notifications&email_token=AAASYU6MEZ3MK2H3NLRNWCLQLZOSHA5CNFSM4E4IWBCKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD7Z44FQ#issuecomment-536071702>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAASYU6KRY243PP7A6YREO3QLZOSHANCNFSM4E4IWBCA>
.
|
I've already posted how to handle edit texts as a previous comment in this issue: There's nothing else to it, it's not complicated. |
So what I have is a TextWatcher in my ModelView class, which calls a Callback (which is set via a @CallbackProp annotated setter) whenever the text changes. Works so far, but when the user enters text quickly I'm getting this error from epoxy:
Referring to you:
I guess exactly the part of handling the TextWatcher is the problem here. So yeah, if could please show us this part would be highly appreciated. |
@zirkler your problem is that you are updating the property on the model. As you can see in the error that is not allowed.
In this simple example the data is stored in a property on the controller. It doesn't matter to epoxy where you store it, it just can't be on the model. A good app architecture will have a good pattern for storing this state, but that is outside of the realm of Epoxy (See https://github.com/airbnb/MvRx for a good pattern for state) In general the pattern is straightforward:
That's all there is to it! |
Thanks a lot for your answer, in fact I am using MvRx, great pleasure to work with it. I just found out my issue was caused by the mutability of CharSequence. In my MvRx State I store CharSequence objects and the setInputText accepts a CharSequence. When logging the CharSequence and its corresponding idendityHashCode in the TextWatcher, I realized the mutation in the model was caused by the CharSequence object. To fix this I now simply create a new object by calling Thanks so for your patience, I consider this Issue closed for now. |
Thanks for sharing what the problem is, glad it is working now! |
Finally had a few hours to look into what has been going wrong with my implementation of forms. So far it's looking like the deferred model building was actually being overridden immediately because I'm also investigating your statement that we shouldn't be converting to So, for now, I've written a bit of code to do the following on Invalidate:
If this is done (and I'm not sure it's the best way to do this, but it's an experiment right now) I notice much more predictable behaviour on the forms, since the model building is actually deferred when desired, instead of firing whenever the MvRx state changes. |
Thanks for sharing @nealsanche. MvRx is a bit different - it will always automatically request a model build (like you discovered) so you never need to do it manually in your code. Do you have your EpoxyController set up to build models and diff them asynchronously? We have found that we don't need to delay our model building with mvrx when the epoxy work happens async, since that seems to delay it enough to not cause cyclical issues with typing. Otherwise an approach like what you have would be find - although it could maybe be cleaned up into a nicer pattern. |
Yes, @elihart, we have been doing async model building with Epoxy. I've been doing a series of experiments. Symptom: Sometimes keystrokes are missed, or backspaces don't clear a character due to a model build that happens at 'just the right time' so that the user's view is out of sync with the model being built. Did a series of experiments to see if these issues can be improved:
return this.subSequence(0, this.length)
} BAD RESULT: Still skipping characters and backspaces leave the last character in a field.
BAD RESULT: Character skipping and backspace issue still present, but will happen much more infrequently such as when the user pauses for just the right interval of time. Have also seen exceptions related to two CharSequences with what looks like the same values (same characters, but perhaps a different cursor location or something) that MvRx throws, and then model building just stops.
Still working on reproducing the problems with this. I'll get back to you. |
hi guys, I just came across this issue and am a bit overwhelmed - especially the last comment with async/deferred model building, etc. Is there any chance that a standalone example with the best practices (including async model building, etc etc) for those of us getting started ? sorry if its a bother for you guys. |
@vnwarrior I felt the very same, but in the end, it's pretty simple. I will put together an example repository which demonstrates how to build forms with Epoxy and MvRx. |
I'm going to spend some time making a minimal example of what we've been doing, see if I can get a reproduction case worked out so we can figure out what the issues are. Clearly having the models rebuilt every time the user inputs text is the root cause of the issues I'm seeing. Specifically with async model building. There's always a chance the async model will be built with out-of-date data. |
So, I also broke out a minimal example of what we're currently doing inside our code. It's gotten pretty complicated, as you can see. As usual, I don't seem to be seeing the bad behaviours that I was within our app, so there might be something else contributing to that. But, in case someone else is interested in how someone else has added validation, data binding, and form handling into an app using Epoxy and MvRx. This might be an example of that. |
hey - thanks guys ! this is super useful (and probably should be added to
the main mvrx examples repo)
…On Wed, Oct 9, 2019 at 1:52 AM Neal Sanche ***@***.***> wrote:
So, I also broke out a minimal example of what we're currently doing
inside our code. It's gotten pretty complicated, as you can see. As usual,
I don't seem to be seeing the bad behaviours that I was within our app, so
there might be something else contributing to that. But, in case someone
else is interested in how someone else has added validation, data binding,
and form handling into an app using Epoxy and MvRx. This might be an
example of that.
https://github.com/nealsanche/EpoxyFormExample
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#426?email_source=notifications&email_token=AAASYU5D7C5JHJ7GMRCOMJTQNTTYTA5CNFSM4E4IWBCKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEAVPQ4Q#issuecomment-539687026>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAASYUZRSSWK56XCXUKOATDQNTTYTANCNFSM4E4IWBCA>
.
|
@nealsanche thanks for sharing your sample! Databinding does work a bit different - I don't know that you need the whole binding setup to check Also, I wouldn't recommend the By default with databinding, The simplest solution to all of this is that when a new text value is set on the view you can check whether the view is attached to the window. If so, it is a rebind of new data and you could ignore it assuming this is text that was already inputted into the view. The first initial bind only happens when the view is detached from the window. This would probably solve your problems and is easy - the big downside is if you ever wanted to programmatically change the content of the edit text while it is on screen you would not be able too (although you could have a function like Lastly, small reminder that this is unnecessary
You don't need a factory if you are just using the default state, so this whole object can be removed |
@elihart Thanks for the info. I have to admit immediately that I don't understand most of it. Just now I spent some time trying to figure out where the TextWatcher is having it's 'DoNotHash' behaviour set. I still see
So, something I'm doing might be wrong here. Data binding layout looks like this:
I'll see what relaxing the isTextDifferent checks do. The reason for this I'd really appreciate a quick example of your statement:
Is this a bit of code wrapped around the setting of the value in the model? Is the View actually available in the model building DSL somewhere other than in onBind? I think maybe the fact that we're using DataBinding is really not helping us out here. Thanks in advance. |
Yeah, I'm not sure if that is possible with data binding. We do it like this in a custom view
|
FWIW I'm doing the following in my controller when using databinding: genericMultiLineTextInput {
id("note")
text(note) // only used on full binds, never on update (not used at all in the layout!)
onTextChangedListener { s, _, _, _ -> // data binding variable defined in layout and bound as text watcher
// store text in ViewModel so that it outlives configuration changes and (by means of additional code) restores
// make sure that this is a really quick call or move to another thread/coroutine
viewModel.updateNote()
}
onBind { model, view, _ ->
if (!view.dataBinding.root.isAttachedToWindow) {
// restore state on full binds (e.g. after restore) and when the item is scrolled back into the viewport
(view.dataBinding as ItemLayoutGenericMultiLineTextInputBinding).textInput.setText(model.text())
}
}
} Seems to work pretty well even when entering test super fast, copy & pasting, deleting, orientation changes, restores etc. |
hey Guys, I am also facing the similar issue. I have been using the data binding approach for Epoxy and somewhat stuck with view update with textWatcher. Do we have any similar example that uses data binding approach (like that we have for mvRx) |
Our team has been using Epoxy now for a few months, but noticed that you can also use it for static content. We first want to address our Login page. It's currently a scrollView with a linear layout, with an imageView, and a few editTexts and a button. How do I use Epoxy for a screen where I would typically be able to directly access all of the views? I didn't see any real docs/wiki surrounding this. Sorry if I missed it.
The text was updated successfully, but these errors were encountered: