I spent 3 years writing an Android Contacts API in Kotlin with Java interop. What I’ve learned…

vestrel00

Vandolf Estrellado

Posted on November 4, 2021

I spent 3 years writing an Android Contacts API in Kotlin with Java interop. What I’ve learned…

Hi, my name is Vandolf and I have a problem. I get addicted and obsessed about certain things. It spirals out of control, very quickly. Once I set my sights on something, I don’t stop until I finish it, even if it takes years. Sounds like addiction… It is.

Before I know it, I've spent three years working on private repositories building something no one knows about. Then, I make it public and write an article about it thinking that it will blow people's minds. Then get shocked that the repo has less than 100 unique visitors in the first month because I think that it will just magically get discovered by everyone...

Does this sound relatable to you? Probably not, right? I think I'm insane in some levels. If you are interested in the Contacts API library I've built or the insanity I'm going through, keep reading!

I'll split this article in two sections.

  1. The Contacts API that I've built. 🔥
  2. The mistakes I made and lessons I learned from writing "open source" code, privately, for three years. 🤦‍♂️

If you are not interested in the first section, I recommend at least reading the second. You might learn something from the mistakes I've made, and hopefully avoid making them yourself!

I'm actually a bit hesitant and nervous about posting this as my first article here. I have no reputation here. No one knows me. I'm just a random stranger. However, I know that doing things that are only within my comfort zone will not get me anywhere. So, I'm pushing myself to publish this article 😰

I hope to make connections, and maybe even friends, in this community ❤️

The Contacts API that I've built.

First, here is the GitHub repo; https://github.com/vestrel00/contacts-android

Please take a look at it and let me know what you think?

The oldest commit in the repo is 2 years old. However, I have another private repo that has the 3 year old commits. I'm not lying! 😁

Exactly a month ago from today, I have made public the previously private repo and published an article in Medium introducing my Contacts API library to the ether. The article covers the history of Contacts in Android since Android API 1 to API 5 (Eclair) all the way to API 31. It talks about how, for over a decade, there has not been a single open source library that abstracts away all of the complexities of Android's Contacts Provider and ContactsContract. The community has been forced to deal with ContentProviders and cursors, spending a plethora of hours of trying to figure out how to do even the simplest things. Then it goes through an overview of the Contacts API library that I've built and how you can use it to create your own full-fledged Contacts app in Kotlin and/or Java.

A hypothetical, but probable, story.

Image description

I won't repeat myself or copy-paste things so I'll take a different approach in this article. I'll go through how a normal person might use the native Contacts application and show code snippets on how you can implement those use cases using Contacts API, Reborn. To make things easier to follow, let's go through a hypothetical day of someone named Vandolf.

One day, Vandolf takes his phone and opens up the Contacts app. He's bored so he decides to look through the contacts he has saved over the years.

val contacts = Contacts(context)
    .broadQuery()
    .include(Fields.Contact.DisplayNamePrimary)
    .orderBy(
        ContactsFields.Options.Starred.desc(),
        ContactsFields.DisplayNamePrimary.asc(ignoreCase = true)
    )
    .limit(20)
    .offset(0)
    .find()
Enter fullscreen mode Exit fullscreen mode

He sees favorites at the top of the list (which includes his wife, son, dogs, and cat) followed by non-favorites, ordered by the contact display name. Vandolf scrolls down to see more.

val contacts = Contacts(context)
    .broadQuery()
    ...
    .limit(20)
    .offset(20)
    .find()
Enter fullscreen mode Exit fullscreen mode

One of his high school friends popped up. Curious about his ancient friends, he types a name in the search field.

val friends = Contacts(context)
    .broadQuery()
    ...
    .whereAnyContactDataPartiallyMatches("henry")
    .find()
Enter fullscreen mode Exit fullscreen mode

He sees a bunch of contacts with the name "Henry". Determining that it is in fact the same Henry, he decides to link them all together into a single contact.

listOfHenries.link(Contacts(context))
Enter fullscreen mode Exit fullscreen mode

He then searches for Victor and finds that he only has one contact with that name. He knows that he has 3 friends named Victor so he opens up that contact and discovers that the Contacts Provider mistakenly combined 3 different Victors into a single contact. Facepalming, he decides to unlink the Victors into 3 separate contacts.

victor.unlink(Contacts(context))
Enter fullscreen mode Exit fullscreen mode

Continuing his mindless quest of browsing his contacts, he looks through his contacts one account at a time.

contactsApi
    .accounts()
    .query()
    .find()
    .forEach { account ->
        val contactsFromAccount = contactsApi
            .broadQuery()
            ...
            .accounts(account)
            .find()
    }
Enter fullscreen mode Exit fullscreen mode

Then he realized that he only wants to see contacts from all of his Google accounts.

val contactsFromGoogleAccounts = contactsApi
    .broadQuery()
    ...
    .accounts(
        contactsApi
            .accounts()
            .query()
            .withTypes("com.google")
            .find()
    )
    .find()
Enter fullscreen mode Exit fullscreen mode

Not finding what he is looking for, he decides to browse contacts by label (group membership).

contactsApi
    .groups()
    .query()
    .find()
    .forEach { group ->
        val contactsInGroup = contactsApi
            .broadQuery()
            .groups(group)
            .find()
    }
Enter fullscreen mode Exit fullscreen mode

Unable to find "that person", he decides to do an advanced search.

val thatPerson = Contacts(context)
    .query()
    ...
    .where {
        (Name.GivenName startsWith "Jose") and
        (Email.Address { endsWith("gmail.com") or endsWith("hotmail.com") }) and
        (Address.Country equalToIgnoreCase "US") and
        (Event { (Date lessThan Date().toWhereString()) and (Type equalTo Event.Type.BIRTHDAY) }) and
        (Contact.Options.Starred equalTo true) and
        (Nickname.Name equalToIgnoreCase "Dota 2 Pro") and
        (Organization.Company `in` listOf("uber", "eats", "door", "dash")) and
        (Note.Note.isNotNullOrEmpty())
    }
    .find()
    .firstOrNull()
Enter fullscreen mode Exit fullscreen mode

Vandolf got tired of browsing. He decides to make a new contact for his ol' imaginary friend.

val contactsApi = Contacts(context)
val accountToAddContactTo = Account("vestrel00@pixar.com", "com.pixar")

val insertResult = contactsApi
    .insert()
    .forAccount(accountToAddContactTo)
    .rawContact {
        setName {
            givenName = "Buzz"
            familyName = "Lightyear"
        }
        setNickname {
            name = "Buzz"
        }
        setOrganization {
            title = "Space Toy"
            company = "Pixar"
        }
        addPhone {
            number = "(555) 555-5555"
            type = Phone.Type.CUSTOM
            label = "Fake Number"
        }
        setSipAddress {
            sipAddress = "sip:buzz.lightyear@pixar.com"
        }
        addEmail {
            address = "buzz.lightyear@pixar.com"
            type = Email.Type.WORK
        }
        addEmail {
            address = "buzz@lightyear.net"
            type = Email.Type.HOME
        }
        addAddress {
            formattedAddress = "1200 Park Ave"
            type = Address.Type.WORK
        }
        addIm {
            data = "buzzlightyear@skype.com"
            protocol = Im.Protocol.SKYPE
        }
        addWebsite {
            url = "https://www.pixar.com"
        }
        addWebsite {
            url = "https://www.disney.com"
        }
        addEvent {
            date = EventDate.from(year = 1995, month = 10, dayOfMonth = 22)
            type = Event.Type.BIRTHDAY
        }
        addRelation {
            name = "Childhood friend"
            type = Relation.Type.CUSTOM
            label = "Imaginary Friend"
        }
        groupMemberships.addAll(
            contactsApi
                .groups()
                .query()
                .accounts(accountToAddContactTo)
                .where {
                    (Favorites equalTo true) or
                    (Title contains "friend")
                }
                .find()
                .toGroupMemberships()
        )
        setNote {
            note = "The best toy in the world!"
        }
    }
    .commit()

val olImaginaryFriend = insertResult.contacts(contactsApi).first()
Enter fullscreen mode Exit fullscreen mode

Of course, Vandolf adds a photo of good ol'Buzz.

fun onClickPhoto(activity: Activity) {
    activity.selectPhoto()
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    onPhotoPicked(requestCode, resultCode, data,
        photoBitmapPicked = { bitmap ->
            olImaginaryFriend.setPhoto(contactsApi, bitmap)
        },
        photoUriPicked = { uri ->
            val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, uri)
            olImaginaryFriend.setPhoto(contactsApi, bitmap)
        }
    )
}
Enter fullscreen mode Exit fullscreen mode

Vandolf then decides to make some changes to his new contact.

val updateResult = contactsApi
    .update()
    .contacts(olImaginaryFriend.mutableCopy {
        setNickname {
            name = "The Buzz"
        }
        removeAllPhones()
        removeWebsite(websites().first { it.url?.contains("disney") == true })
        events().first().apply {
            type = Event.Type.ANNIVERSARY
        }
        setNote {
            note = "The best toy in the world - EVER."
        }
    })
    .commit()
Enter fullscreen mode Exit fullscreen mode

He also sets the first email to be the default/primary email.

olImaginaryFriend.emails().first().setAsDefault()
Enter fullscreen mode Exit fullscreen mode

Once Vandolf has finished his business in the toilet, where this entire story has been taking place 😮, he decides that the whole exercise was stupid and deletes his olImaginaryFriend.

val deleteResult = contactsApi
    .delete()
    .contacts(olImaginaryFriend)
    .commit()
Enter fullscreen mode Exit fullscreen mode

I'll stop the story here. This article is getting too long and I've probably lost 95% of the readers at this point.

That story barely scratched the surface of the library.

There are a lot more APIs in the library that remains untold. The story would be too long if I wanted to showcase everything that the library can do. I’d have to write a novel!

One thing I will mention here as an extra is that all of the functions I’ve shown in the story do not handle permissions and also does the work on the call-site thread. I left those out for brevity and to show that the APIs are framework-agnostic. Anyways, the library provides Kotlin coroutine extensions for all core functions to handle permissions and execute work outside of the UI thread.

// Use the *WithPermission extensions to ensure permissions are granted prior to querying.
// Use the *WithContext or *Async extensions to execute the operation outside the UI thread.

launch {
    val contacts = Contacts(context)
        .queryWithPermission()
        ...
        .findWithContext()

    val deferredResult = Contacts(context)
        .insertWithPermission()
        ...
        .commitAsync()
    val result = deferredResult.await()
}
Enter fullscreen mode Exit fullscreen mode

Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes.

All APIs in the library are optimized.

Some other APIs or util functions out there typically perform one internal database query per contact returned. They do this to fetch the data per contact. This means that if there are 1,000 matching contacts, then an extra 1,000 internal database queries are performed! This is not cool!

To address this issue, the query APIs provided in the Contacts, Reborn library, perform only at least two and at most six or seven internal database queries no matter how many contacts are matched! Even if there are 100,000 contacts matched, the library will only perform two to seven internal database queries (depending on your query parameters).

Of course, if you don't want to fetch all hundreds of thousands of contacts, the query APIs support pagination with limit and offset functions 😎

Cancellations are also supported! To cancel a query amid execution,

.find { returnTrueIfQueryShouldBeCancelled() }
Enter fullscreen mode Exit fullscreen mode

The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query.

This is useful when used in multi-threaded environments. One scenario where this would be commonly used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text.

For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled,

launch {
    withContext(coroutineContext) {
        val contacts = query.find { !isActive }
    }
    // Or, using the coroutine extensions in the async module...
    val contacts = query.findWithContext()
}
Enter fullscreen mode Exit fullscreen mode

All core APIs are framework-agnostic and works well with Java and Kotlin

The core APIs in the core module does not and will not force you to use things you don’t want. As a matter of fact, the only dependency the core module has is the standard Kotlin lib! Use whatever Java or Kotlin permissions or threading library you want. The extensions are optional and are there for convenience.

I also made sure that all core functions and entities are interoperable with Java. So, if you were wondering why I’m using a semi-builder pattern instead of using named arguments with default values, that is why. I’ve also made some other intentional decisions about API design to ensure the best possible experience for both Kotlin and Java consumers without sacrificing Kotlin language standards. It is Kotlin-first, Java-second (with love and care).

Modules other than the core module are not guaranteed to be compatible with Java.

The library provides full documentation in code and with how-to pages. The project also has a vision and a roadmap.

Documentation and how-to guides are all available and linked in the repository. You can browse the Howto pages or visit the GitHub Pages. Both contain the same info but the GitHub pages are not guaranteed to be up-to-date. The GitHub wiki hosts the project roadmap. It contains all planned work and release schedules, which are organized using issues, milestones, and projects.

The mistakes I made and lessons I learned from writing "open source" code, privately, for three years.

For the past three years, I've had my head buried in the sand. I kept myself out of all social media. No Facebook. No Twitter. No Instagram. No Reddit. No Medium. No Dev.to. I got my world news from several news outlets. I'm subscribed to AndroidWeekly and KotlinWeekly to keep myself up-to-date in the world of Android, given that my entire career hinges on it.

For the most part, it was a great experience. By "it" I mean real life. We all know social media can be a detriment to one's health. I don't regret it at all. I can say with 100% confidence that I am healthier now because of it.

I have been working at a great company and spending time with my family. When I find free time, I work on this project.

However, now that I have made this passion project public, I'm coming to a stark realization that I should not have completely disconnected myself from everyone. At the very least, I should have kept myself active in the open source community that I'm attempting to be a part of.

Mistake #1 - Writing "open source" code, privately.

Image description

Do you see the paradox in that statement? Really, what was I thinking 🤦‍♂️. I know it's a classic mistake and I'm not the only one that has made this mistake. Some of you reading this may have also or is in the process of making this mistake.

First, it is useful to understand why people do this. Why I did this. For most, it's something along the lines of "I don't want to show it to others until it is done". While I can relate to that, for me, it was really about not having the time to participate in open source and collaborate.

In my mind, and I could be 100% wrong here given that this is the first library project that I encourage others to use, had I made it public from the beginning and successfully pulled in others to contribute, I would not have had the same flexibility and agility to build the API the way I wanted to. I would have had to convince other contributors that this is the way to go and spend time collaborating to come to the best possible solution. Collaboration takes time. To some, especially folks like me, it is a commitment. At the time, my son was just born and ever since then, finding time to do anything for myself has been very difficult.

The mistake that I made here is that I missed the whole point of open source. The very thing that I was trying to avoid because I had no time is the thing that makes something open source. It is as much about the collaboration than it is about the output. Me building something privately, then making it public does not suddenly make it open source. It just means that I built something and other people can see and use it now.

My code, even though it is public now, cannot be considered open source (even though the source is open).

Mistake #2 - Thinking that no one else in the world is building what I'm building.

Image description

Another classic mistake. My thought process here was that since no one has attempted to wrap the entirety of the ContactsContract for the last decade, why would anyone do it now? There are so many contacts management apps out there already. Surely, a simplified Contacts API is not in demand. No one needs it. No one is working on something like this except me 💥

While I knew deep down that this mindset was completely wrong, I lied to myself thinking that I must be the only one to discover this missing basic-but-critical component in mobile development. There are 6+ million Android devs in the world but I must be the only one that have scoured the internet searching for a simplified Contacts API so that I can use it for work...

Because of this mistake, I now wonder how many opportunities to collaborate with like-minded individuals have I missed?

Mistake #3 - Not participating in the open source community.

Image description

I claim that I want to contribute and be a part of the open source community. However, I don't even make the effort to contribute to the open source libraries I use on a daily basis for work (e.g. RxJava, Glide, etc) because I don't think I'm smart enough. So, I figured that one way I can participate in open source was to work in secret by myself for years and then promote my secret-made-public code as "open source". What a joke! It is a farce. 💩 💩 💩

I clearly misunderstood what open source is.

What I have now is a not-open-source-but-public repo. 😆

Anyways, because of my isolationism, I have no reputation in any developer communities even though I’ve been working for almost a decade now. This lack of reputation makes it extremely difficult to spread the word about stuff I want to share. For example, like most communities, the subreddit r/androiddev has a self-promotion rule that states that the maximum amount of posts you can do to promote your own creation is 50%. This is a big problem for me because up until less than a month ago, I didn’t even have a Reddit account!

I will not post things that I’m genuinely not interested in just so that I can game the system. I have really just been interested in this project for the past three years.

❓ Is that wrong?
❓ Is there room for people like me who zoom in on one thing, spend years on it, and share once they deem it presentable?
❓ Or are we living in a world of instant gratification where all you have to do is spend a few hours, days, or weeks on something and then proceed to market it?

One stroke of good luck I had was when my manager at the job I had four years ago encouraged (or maybe even forced?) the entire team to write dev articles, even while at work. I wrote Howto articles about dagger-android in Medium under the ProAndroidDev publication. The first ever article I wrote made it to issue #268 of AndroidWeekly! I did spend months on the accompanying GitHub project so it was nice to see my hard work get recognition. At the time, I didn’t really think much of it. The editors of ProAndroidDev did all the marketing for me. I told them that I had no Twitter account. No Reddit account. I didn’t even know what AndroidWeekly was. From this, I managed to gain some followers and enough of a rapport with the ProAndroidDev editors, albeit four years ago, to allow me to get my newest article published under their publication.

I have to keep in mind that having connections give you an incredible advantage that you can use as a platform to boost yourself up initially. It applies to everything. Landing your dream job. Meeting like-minded people. Getting a reservation at a restaurant (pre-COVID) that is always booked, let's say Dorsia? Most relevant to this article, having a reputation (or enough posts/karma) in forums such as r/androiddev allows you to self-promote when the time comes. A luxury that I currently do not have.

This leads me to the next mistake...

Mistake #4 - Hoping that people will magically discover what I've built without promoting it.

Image description

I truly believe that a "good software or product will sell itself". It is a common expression for a reason right?

❓ So, if my repo is not getting any traffic, then it must be because what I built sucks, right?

As I mentioned earlier in this article, I already published an article in Medium a month ago. That was it for my promotion. I submitted a link to the medium article and my repo once to AndroidWeekly and KotlinWeekly but to no avail. I even emailed the curators of those legendary mailing lists explaining the amount of work and love I put into this project and how I think others should know about it. They are not responding. I don't blame them. Why should they look at or respond to some random person with no reputation when they probably have to sift through hundreds, if not thousands, of submissions?

A month later, my repo has had less than 100 unique visitors (some of which are probably not even human). I'm not hunting for stars! I’m hunting for traffic. I sounded like an advertiser there... I don't know how else to say it. We all have to "advertise", at least a little bit in the beginning to kick things off. I feel that I owe it to myself and my family, for putting up with me sometimes spending a lot of time on this instead of with them, to get the repo looked at by enough human eyes to justify the amount of time that I've spent on it.

It just happens to be that the easiest way to know if the community thinks a project is good is based on the amount of stars divided by the amount of views. The star/view ratio provides repo owners a heuristic calculation of how many people find the repo useful versus those that see it and just click it away. I could be completely wrong here, in which case please leave a comment and let me know your thoughts about it?

I'm not trying to sell anything. I'm trying to help people solve their problems with anything related to Android Contacts management. I'm not going to get money from this. Will I get attention and fame? Maybe? I really don't want that, honestly. If that’s what I wanted, I would be doing things involving hot new stuff like Jetpack Compose instead of something that’s been around for over a decade.

I might be contradicting myself here but I really just want to make sure that other Android devs out there are aware that they no longer have to fear Contacts like I did. There is now a Jetpack-like component that is fully documented with Howto guides on everything they can think of. They no longer have to scour the internet to find something that does not exist.

At the end of it all, as long as I have truly helped a handful (and I mean less than five) of other devs out in the world, then I have no regrets. Maybe I'll get sincere emails from three people letting me know how much my work helped them? Regardless, I will continue working on the project until I deem it complete and all issues have been closed.

❓ BUT, how can others know the existence of the API I created if it does not appear on any search results in any search engines?

Luckily, this is a mistake that I can still fix, hopefully... And no, I will not fix it with money. I will not pay to be featured in any mailing lists or to appear at the top of search results. I want the community to decide if what I had built should be used by others or not.

So, I still think that "a good software or product will sell itself". However, I'll make a minor modification to that.

"A good software or product will sell itself, once enough people have seen it."

I will not spam the community. However, I will write genuine articles like this from time to time with new content I have not yet shared. By the way, this article took me around 10 hours to write, proof-read, rewrite, and repeat. I hope that the Dev.to community can consider that enough of a justification for this not to count as spam.

Mistake #5 - Answering ancient questions in StackOverflow using the APIs that I've built.

Image description

This one is tragic comedy. I was not aware about self-promotion rules in StackOverflow or dev forums in general. After all, I have just started participating in developer forums. But bliss cannot be used as an excuse!

Anyways, I always thought that I could answer all contacts-related questions in StackOverflow after making my library public. After all, most of us find questions and answers in StackOverflow. So what better way to make sure people know about my API than answering questions in the stack?

Excited and full of energy and determination, I began answering decade-old questions in the stack. I used a template for all the questions I answered. I did not save it, nor do I have access to them now, but I remember it being something like...

Android Contacts has just been Reborn! Using the Contacts, Reborn library, you no longer have to deal with the complexities of ContactsContract.

To answer your original question...

"(quote original question here)"

Using the Contacts, Reborn library...

In Java,
(insert code snippet of how to solve the problem in Java using my library)

In Kotlin,
(insert code snippet of how to solve the problem in Kotlin using my library)

There is a lot more that the Contacts, Reborn library can do. You can even build your own full-fledged contacts app with it!

It's probably not exactly that but it's the best that I can recall.

Anyways, after around ten hours of answering around 20-30 questions, I continued to answer more questions. However, when I tried to post an answer on October 8, I got the message saying something like "this account can no longer post answers". Then, I noticed that I had a message from the mods. It said that I violated self-promotion rules. So, they removed all of the answers I posted. All of that hard work, excitement, and determination, vanished into thin air. Gone. Just like that. It hurt. It really did. I was sad, furious, emotional. However, I understood that rules have to be followed even if I did not know them. Bliss is no excuse!

I took a screen recording of the message thread in my phone. Here is a screenshot of the message I got from the stack mods. I was too emotional at the time when I was reading it. In retrospect, I could have taken a different approach to handling the situation 😅

Image description

Again, in retrospect (reading it again now), I could have handled the situation differently. I could have still said what I had to say (that I'm hurt) but just try again and only answer a few questions instead of trying to answer all of them. I had over a hundred questions bookmarked and I was ready to spend tens of hours to answer them all!

So, I sent them a heartfelt reply and then deleted my account. On a side note, it takes 24 hours for an account to get deleted in the stack. It's probably there to give emotional folks like me some time to calm down after getting the well-deserved hammer. There is even a message, and I paraphrase, "we noticed you just received the ban hammer, calm down and reconsider deleting your account?"

I know you are curious to see the message I sent. Everyone loves a bit of drama, even if it isn't your favorite genre 😆. I consider this tragic comedy! I'll share it with you all. Only here in DEV.TO! Don't worry, it does not contain profanity or anything of the sort. It is not not-safe-for-work (double negative). Even though I was emotional, my head was leveled enough to avoid sending a self-career-ending reply 😌. I knew who I was talking to. I was talking to hard-working mods that have to sift through hundreds and thousands of posts and make hard decisions like this knowing that they will get backlash from individuals. I was talking to people that keep the stack community alive and well for everyone (including you and me) to find answers. I was taking to people that have helped me get to where I am now in life. Who am I to disrespect them? I can't. I won't.

Before I share a screenshot of my reply, please know the following.

  1. I'm sharing this with you because I want to share my mistakes so you can avoid doing it yourself.
  2. I am not trying to hurt the StackOverflow community. If anything, I’m trying to help the moderators by having folks read my story as a cautionary tale!
  3. I respect, love, and admire, folks that are active in the StackOverflow community. I was just trying to be like them... but I failed.
  4. I still use StackOverflow to get answers, anonymously for now. I hope to create an account once again, when I feel like I deserve it.

Without further ado, here is my reply...

Image description

Reading it now after almost a month, I can see my heightened emotions. I made several grammatical errors as I only proofread it once. I was well aware that I was emotional when writing that at the time. I am typically patient and I wait to do things until I'm level-headed. However, I intentionally decided to write that reply immediately because I wanted my emotions to come through. I wanted to show them how it affected me and probably folks that have and who will experience the same thing that I did. I knew that if I had waited even just an hour, I would not be able to write a reply that conveyed what I truly felt. I don't regret writing that reply the way it is written. I felt like it had to be written that way. Otherwise, I would be doing myself (and others like me) injustice.

I know for sure that I'm not the only one that went through this experience. Have you gone through this? How did you handle the situation? I'd love to know!

Thank you for reading!

Image description

Whether you read all of this article or just some parts of it, thank you ❤️ 💗 💕 💞 💘 💖

I give every bit of love and care to this project, including the articles I write about it. I would appreciate any feedback. What do you think about the library? Did you make the same mistakes I made? Do you agree with the lessons I've learned? Any other thoughts? I sprinkled in some questions throughout the article, care to share your thoughts on any of them?

P.S. I am working on a fun app that uses the library that I've built. It is already in a public repo. I still need to work on it a bit before I start ranting about it and getting others to contribute. I am excited about it though! This time, it will truly be "open source" ❤️

💖 💪 🙅 🚩
vestrel00
Vandolf Estrellado

Posted on November 4, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related