klinker24/Android-TextView-LinkBuilder

Support for CharSequence

ZacSweers opened this issue · 9 comments

Currently, LinkBuilder only supports taking in a TextView param and setting the text on it directly. This is useful, but a little restricting if the text is only part of a process.

Example:

Say you have a JSON object that comes down from the server. This object has some text that needs to be linkified, and it would be nice to apply that linkification as part of the JSON conversion and store the result in a CharSequence field (which SpannableString implements).

It would be nice if there was an overload for LinkBuilder's constructor that took a CharSequence or String instead of a TextView, and offered a buildCharSeq() method or something similar that just returned the linkified SpannableString as a CharSequence.

What do you think? If you're open to it, I'd be happy to take a crack at pull requesting this.

To be honest, I don't really see a use for this one. I mean, to do anything with that SpannableString, you would have to apply it to the TextView anyways...

Or am I missing something here? Just doesn't seem like it would add all that much to me and it would make for a much more confusing implementation, in my opinion

A few good reasons:

  • Increase testability (test the span without having to go through the TextView). It's also considered good practice to not have methods that mutate their params, as it keeps code simple and enforces immutability where possible.
@SmallTest
public void testSpannableString() {
    SpannableString test = (SpannableString) Util.getLinkifiedSpannableString("Look at this link http://i.imgur.com/ZsFISaE.jpg");
    assertTrue(test.getSpans(0, test.length(), URLSpan.class).length == 1);
}
  • Currently this implicitly requires a Context via the TextView, which doesn't play nice when dealing with threading, let alone testing.
  • It ties it to the lifecycle of the TextView (if I rotate, do I really need to do all the linkifying again?).
  • Two tie together the last two points: Modularizing this allows a developer to push all of the regex matching and string finding to background threads. Micro-optimization, but it does tie in well with the reuse mentioned in the bullet above this if you have caching involved.
public Observable<List<RedditComment>> getComments(String post) {
    return Observable.just(post)
            .map(client::getSubmission)
            .map(Submission::getComments)
            .map(CommentNode::getChildren)
            .flatMap(Observable::from)
            .map(commentNode -> {
                RedditComment redditComment = new RedditComment();
                redditComment.comment = commentNode.getComment();
                redditComment.body = Util.getLinkifiedSpannableString(commentNode.getComment().getBody());
                return redditComment;
            })
            .toList()
            // And now we can get back on the UI thread
            .compose(RxUtil.applySchedulers());
}

Also I just realized I was incorrect in my original post. The constructor would have to take in the String or CharSequence to linkify, not be an empty constructor. Updated it now.

The increased testability I understand, however, it would be much more useful to write tests for the library itself rather than each developer doing it individually for their apps. This is not something I have gotten around to though.

As for the threading and lifecycle, I suppose that is a valid reason. What is stopping me from saying yes is the complexity it would add. This would help a small subset of developers but for the most part, it would end up making things much more confusing, when the simplicity of the current implementation is one of its strongest points in my opinion.

If you are going to attempt this, how would you do it? I don't think that adding a new constructor and a method is the way to go because it doesn't point the developer to the requirements of using that method. For example, they could try to use the LinkBuilder.build() method without a TextView attached. Which wouldn't do a whole lot of good and again, defeats the simplicity.

So, how would you recommend going about this?

I can understand that, and think the simplest approach (as far as not breaking the current API) would be to add a constructor overload and modify the build() method to return the linkified CharSequence, then act accordingly in the build() method depending on what your initial constructor params are. This isn't particularly ideal since it's effectively confusing two purposes, but it's the only way I can think of that maintains the current API.

A more involved approach would be to tweak the API to pull out textview linkification into its own method where the TextView is a param there, allowing you to add it only when you want it. This is probably how I would do it in a PR.

// Linkified CharSequence creation
CharSequence linkifiedText = LinkBuilder.from(inputText)
                                     .addLinks(getExampleLinks())
                                     .build();

// With a TextView
LinkBuilder.from(inputText)
        .addLinks(getExampleLinks())
        // This would use myTextView's current text if none is provided at construction
        // Calls build() internally
        .linkifyTextView(myTextView);

The LinkBuilder.from(...) approach is just my personal preference here, not necessary.

It does change the API though. You could mitigate this by deprecating the LinkBuilder(TextView) constructor and start having build() return CharSequence. This would give users a good heads up and time to adjust, allowing for use of the new API, and not break the existing one.

Let me know what you think! I always hate suggesting things that would break the current API, but in this case I think it would be worthwhile. It keeps the simplicity while also making the API more flexible to users that prefer to abide by some of the conventions/use cases I mentioned above. Best part being that you don't have break the current API immediately (or ever really, but that might not be the best idea).

I don't want to break the current implementation. I think the best way would be to modify the build method to return the SpannableString. I would then add an empty constructor and add a method to set the TextView and one to set the text (String or CharSequence). You will have to throw an exception in the build method if the user has not set the text or the TextView though.

I say go for it if this is what you want to attempt, I do want it to continue working as-is though as well. Thanks for any help and the discussion

Cool. Probably won't have time until this weekend, but I'll pull request with those changes then!

Thanks for the help!

Closed with 1c90a20

Gah I completely forgot about this! Thanks anyway though!
On Mon, Jul 27, 2015 at 7:11 AM Luke Klinker notifications@github.com
wrote:

Closed #3
#3.


Reply to this email directly or view it on GitHub
#3 (comment)
.