Zebra Datawedge example: Scanning barcodes and catching intent
In the early stages of this project I didn't know very much about Brickx or its customers yet. This meant that I had to look into their needs to try and realize a suitable app for them. I started out by interviewing them and making various UI sketches. You can see the interview here and some of the UI sketches here. Just the designs aren't going to get us all the way however. Because I chose new techniques like Dagger2 and Zebra I made a proof of concept to test if my vision was even possible.
First off I wanted to make sure the hardware this project was committed to was suitable. I looked into their documentation and followed a tutorial that took me through the process of scanning a barcode and then catching the scanned data in the app via an intent.
There are a couple of steps to this process. First, an intent filter string must be set. In the case of the example it was "com.dwexample.ACTION". This is the name of the project root followed by the identifier you want to give your action. In this case that was simply "ACTION". Next, inside of the Datawedge configuration app, you need to specify that the app is using "Broadcast intent". Furthermore you also need to enable output via intent instead of keystroke output.
Once all of the above is set you can make your app and use the barcode scanner. The actual implementation to catch the scanner's data and make the scanned code appear on screen is fairly simple. I was pleasantly surprised with how painless using the barcode scanner was. A snippet of the required code can be seen below.
Parts of this same code were used in the final project.
Zebra Datawedge: example code for using the barcode scanner.
The classes needed to navigate from the main menu to the product info screen. Presenters and navigators are injected using Dagger2.
The app's AppComponent class. The @Singleton annotation here creates an instance of all classes within the component and "kicks off" the Dagger2 dependency injection.
Dagger2 is an dependency injection framework. What that means is that it uses annotations to create instances of classes and can pass around these instances as dependencies so that every injected class is always synchronized.
If you've never worked with this before it can be hard to understand. I have had an introduction to dependency injection in semester 5 of my Fontys HBO career but I've never used it much for Android before, especially not by myself. So a proof of concept was in order.
The idea was to structure a two-page app with an MVP architecture. The program would then inject a navigator in the first screen so that it can go to the second page. If the app navigates, the dependency injection worked. If not, it would be back to the drawing board.
There was also a presenter present that would also be provided. Every view has its own module that gets provided to the app's root. This is to start Dagger2 and make it create instances of to-be-injected classes.
Let's walk through how it works in steps:
This same sequence is used if a calls on the presenter or even the activity itself. The beauty of this approach is that injected classes are globally shared so there can be no more issues with different instances of a certain class being desynchronized.
After I had tested out Dagger2 and Datawedge successfully I was ready to start building a first version of the app. For this I used the development branch in Git. Once I set up the MVP structure and the three modules (presentation, domain and data) I switched to a new feature branch named "feature_authentication". In this branch I made my first API request.
One of the main improvements I wanted to make over the old app was its speed. In an attempts to maximize this I wanted every call to the data layer to be asynchronous. That means that operations that go to the data layer will be processed on a different thread than the UI elements making for a more responsive and seamless user experience.
RXJava can basically be seen as a thread manager. With it you can easily "subscribe" threads to certain operations. It even comes with baked in methods so you can easily program what happens when the request is cut off or completed. Managing threads can get really complicated, especially in bigger programs so that's why I'm letting RXJava do all the heavy lifting. An example of how an RXJava call looks can be seen below.
Retrofit is a REST client for Java. It makes it easy to manage API calls and JSON data. It can be injected by Dagger2 along with the converter of my choice. In this case that would be Google's GSON converter. If you want to see what a call with Retrofit looks like, I added an example below.
RXJava call for getting product info. In this example I call the API on a new thread but I handle the result on the main thread since that's the only thread that's allowed to update the UI. I also add the call's result to a disposable list so it can be garbage collected easier when it's no longer needed.
Retrofit example. I included the whole repository class for this call because it's interesting to see how Dagger2, Retrofit and RXJava all come together in one class. The context is also injected here and I do this more often since certain classes need the context to acces some one the device's sensors or storage. This way I can inject it into any class without passing the context to unnecessary parents or children.
The discussed method of communicating with the API is used all throughout the project. Because of the MVP architecture a lot of the code tends to be similar to each other. For instance, every activity has a presenter, navigator, contract and module. As you can imagine, this means that a lot of duplicated code is being created as every instance of those classes are similar if not the same in structure. This is expected for an app using this architecture. I could show more of the code here but there is honestly a bit much of it so if you're interested, you can always look at the source code on Git.
The original SonarQube analysis.
SonarQube report after going over the bugs and code smells.
The security threat SonarQube found. This was duplicated 7 times.
SonarLint and Qube were used to maintain code quality.
Originally the code had a few bugs and a lot of vulnerabilities. It also had 941 code smells. Something else that's notable is the test coverage. It says 0% and that's mostly true. In the chapter below you can read why there were little to no tests made. Furthermore, the amount of code duplication was rather high as well. This actually normal for an app using the MVP structure. The reason for this is the fact that every single activity package contains at least three standard classes namely:
These classes are often very similar to each other and that gets picked up by SonarLint as a code duplication. Another reason for the duplication were the GSON models that were made. For Retrofit to play nice, a complete copy of the to-be-received models from an API is required. This meant that I had to make a lot of GSON models and often times they contain child models that are a 90%+ match with other GSON models.
The amount of functional lines of code that were duplicated are actually quite low.
After seeing the first scan I was pleasantly surprised at the fact that there were only 2 bugs in the whole project. This is especially nice since the project contains 16.000 lines of code. Nevertheless, all the bugs needed to be solved. The same went for the vulnerabilities and also as many code smells as possible. The result of my code cleanup can be seen in the next image.
As you can see, all the bugs and vulnerabilities are taken care of. There were actually some notable bugs that I will discuss in my bugs article. In addition to the bugs and vulnerabilities, 145 code smells were removed. The security threats still stand however.
I looked into what the security concern were and found that they were talking about broadcast receivers that I had registered. After looking at Google's specifications I realized that there are no risks in the way I implemented the receiver. According to Google, you need to register the receiver in the "onResume" method and unregister it in the "onPause" method. I was already doing this. As long as you do that there should be no leak possible. The code noted in the snippet in the next image was actually copied from DataWedge's own example project so I doubt there is anything else wrong with that either.
Since the app has such a large scale and it also uses dependency injection it is really hard to test. In favor of feature development, little to no unit tests were made and instead the app was tested by users to see if they can find any problems with it. This is also how Brickx usually handles its testing.
Design-wise, paper prototypes were made. Some of them can be seen in the UI segment of this blog. In general the response to the new design was good. Stakeholders liked how the app looked relatively simple yet elegant. This was achieved by the use of material design and card views. Navigation was clear to them as well. Compared to the old app the new app uses different header sizes and colors for text to further improve its clarity.
To make sure the app's design is suitable for the end user I did some usability tests with various people. You can read about the specific test in the document on the left.
One thing to note that wasn't part of the usability test was a complaint that I heard afterwards. A user thought the menu buttons might be a bit too small. They were far bigger in the old app but I made them smaller so that more of them could fit on the screen and minimal scrolling would be required. I'll need to see if other people share this complaint before I change the sizes though.