Manual developer tests, isolate temporary code, and more
Before we begin talking about how to build quality software let’s first define it. Quality software is robust, maintainable, has minimal bugs, and provides a good UX.
Most of the commentary below are general concepts that apply to software development; However, some of it applies more specifically to Android development.
Putting a large initial focus on quality massively pays dividends down the line. With improved code quality, you can increase developer throughput, increase capacity, and decrease lead time which in turn will give you more time to reflect/improve your own process.
This 5-letter acronym outlines general software design principles that help make your code robust. Read more about it here and here.
These are things to keep in mind whenever you write code. These principles help make your code maintainable which is extremely valuable in the long run.
Having a “solid” understanding of all the principles will prove to be very valuable.
When working on a large feature, take adequate time needed to think through how to architect the feature effectively. Think about how all the components will communicate.
Are there any existing design patterns that have already solved the same/similar problem? If this feature needs to change in the future, how can I build it such that change is fast and easy? Ie, minimal ripple effects when making change.
When building a medium-large feature sometimes I’ll take around 4 hours to plan out the architecture. If it’s a very large feature I may even take an entire day or more just to plan out a robust design.
The two main ways to organize code are package by layer and package by feature. With package by layer, you essentially place classes in the architectural layer that they belong to. For example, you might put all your custom view classes in one package, all your repository classes in another package, all your model classes in another package, etc.
With package by feature you place all the classes that are required for a feature in the same package. This has so many benefits! Lower complexity, higher class cohesion, lower coupling between packages. Need to update a feature or maybe remove a feature? Everything you need is in one place — no need to go searching for all the classes that constitute the feature!
This also makes the project more maintainable because future developers will also have an easier time working with the feature because, again, everything is in one place.
There are, of course, things like general utility classes that may be better suited outside a feature package.
For example, if you create a custom view for feature A it’s generally best if you do not reuse that custom view for feature B even if it needs a similar (or the same) view.
Here’s why. Let’s say you create a custom card view that is used by a list on four different screens (feature A, B, C, and D). Today all the card views on the four different screens show the same information. What if the requirements change in the future for only features A and B? What will you do then? Usually one of two things:
- Add some conditions to the custom view and/or more properties so that it can accommodate all requirements for all screens.
- Break off the feature and create a new custom view with the modified requirements.
The first choice is most likely a bad decision because doing so will increase complexity of the custom view. If you keep doing this, over time, you will end up with a monolithic class that is tightly coupled to different features. Now if you want to refactor that class it’s going to take a long time because of the tight coupling to so many features and increased complexity. You might break something in one of the features. Also, keep in mind that with increased complexity comes decreased developer throughput.
The second choice is the better choice, although, it would not have been necessary in the first place if you would have created separate views for each feature.
There is one caveat. If there is a very small custom view that I am highly certain will not change, I may put it in a common view package and use it across features. This might be something like a small, custom user image view that is used within each card view. In contrast, the card view may contain all kinds of information which makes it more likely to be changed.
Take the time upfront to think through all edge cases as you work through features. You should have an understanding of what can go wrong with each part of the code that you work on.
For example, are API errors properly handled? What happens if a user is on their phone and they lose network connectivity? What if a user kills the app in the middle of a long-running operation and then resumes later? What if a user taps a button that makes an API request really fast and repeatedly?
If you cannot address the edge cases immediately then be sure to keep track of them somehow. For example, you can leave a TODO comment. Be sure to address the edge case before you consider the feature complete though!
Android Studio/IntelliJ IDEA has a nice feature where you can see all TODO comments within a package. See this SO post for how to view TODO comments in Android Studio.
Live Templates are a feature in Android Studio/IntelliJ IDEA that allows you to quickly insert boilerplate code. This is an incredibly powerful feature that can increase your productivity (as well as reduce copy/paste bugs).
For example, you can use this when performing repetitive tasks like creating custom views, setting up view binding, writing unit tests, etc. Live templates are customizable using variables.
See this post for most details.
Generally, developers work on tickets created by product managers. The ticket would have the details for a given feature that you’re working on (along with the acceptance criteria).
When working on these feature tickets it’s best to clarify anything that is not clear within the ticket. Do not assume. You will save time by doing this. The last thing we want to do is make the wrong assumption and then need to redo something that could have been clarified earlier.
At my current organization, we have all written communication for a particular feature happening in a specific topic channel within Slack. The relevant people for that feature would be part of the Slack channel (backend engineers, frontend engineers, product managers, etc).
To stay organized, what I generally do is post all questions I have about a particular ticket as a separate comment. Each question can then be answered in a thread for that particular question. I use Slack emoji responses to indicate whether the question has been answered or not. If there are any remaining unresolved questions I use❓to indicate this. If the question has been fully answered then I use ✅ to indicate that status.
No matter how important the feature is, it’s important to take one step at a time and think everything through. Even if you’re “behind” don’t let it get to you. If you’re trying to run as fast as you can, chances are you’ll leave some bugs behind or miss an edge case that could have been prevented.
Even though you’re “going fast” it will take more overall time to come back to fix the issues and the corners you cut. Remaining calm and taking the time that you need will save time in the long run. It’s cheaper to do things correctly the first time rather than fixing it five times after some bugs are discovered.
If you’re feeling stressed, take a break or go for a walk.
Even if there is a critical bug in production you still need to take one step at a time and understand what is happening before you can address the problem. If it will take you too long to fix the issue then you can roll back to the previous version while you work on a fix.
Say you want to quickly start a new activity you’re working on to test it out or maybe you want to simulate some kind of temporary condition in code.
No matter how small the amount of code is that you want to write, if it’s not intended to remain in the code then it should be isolated to its own method. Otherwise, it might blend in with other code and you might forget about it! At least with an isolated method it will be easier to identify.
The integration points are one example of a potential blocker. Suppose you’re working on a new client side feature that requires API integration. Assume that it will take one day to complete the API and 5 days to complete the client side work. It’s a good idea to test the API very early on in the process (like as soon as it’s ready) even if you have not finished building the UI.
If you wait to test the API until you’re completely done building the feature or, worse, don’t test it at all and simply throw a build over the fence to QA, you are risking extending the lead time of the feature. By staying in close communication with the backend team and helping to test out the API before you finish building the feature, you’re helping to reduce the overall lead time.
When you’re done building the feature you should try to break it. Do some of your own manual developer testing to make sure you didn’t miss any edge cases and things work as you’d expect. This will help shorten the feedback loop.
Imagine for a minute that we completely skip doing our own developer tests and simply throw the build over the fence to QA. Then QA finds one bug and then sends it back for you to fix. Meanwhile, you have already switched context to another ticket. Then you go back and fix the bug found in QA. Imagine this happens for one more round between QA and you.
Can you see how this kind of thing could easily extend the development time for a ticket by several days (context switching + spending more time on the same ticket)? Although doing some manual developer testing before passing a build over to QA may seem tedious, it will save you time in the long run.
Ideally, every feature that you build should have unit tests. If the code is not testable then it needs to be improved so that it is testable.
Even better if your unit tests run on every commit to your develop/master branches via continuous integration.
Standard code reviews are another great way to catch errors and potential improvements. A thorough code review is much more valuable than simply clicking the approve button.
Keeping a close eye on crash reports right after a release can prove to be helpful. It’s a great feeling when you can identify and fix errors before they get too large.
Tagging the commits that you release will make your life easy if/when you need to roll back to a previous version. I use tags like
release/1.2.3–456 that indicate the release number.
When you need to roll back you can then easily check out the tagged commit, bump the version, and release the build. This method works even if your project uses submodules.