Tue, 13 Jun 2017
Developing an Android app - Wallpapers app - part 4
This is my 4th post in a series about the Wallpapers Android app I developed that is on Google Play. The first blog post describes how I started developing it, this is about the last few versions I released.
So I started working on this app one year and three months ago. I released version 1 one year and one month ago with 335 wallpapers. I am in the middle of a staged rollout of my most recent release, which was a fairly significant one, as I have been working on the latest release for three months without any intermediate releases since then.
The main thing I did was made the app more explicitly in line with what Google suggested. Google suggested that Android apps be built with certain architecture types. Two of the popular architecture models they suggested were MVP and MVVM. As the MVP (Model-View-Presenter) architecture was the simplest architecture they suggested, and fit with what I was doing, I went with that.
Of course, right as I was finishing up with all the work I had done following Google's then-current best practice suggestions, Google I/O happened and Google announced a whole new official architecture framework. So my app's architecture was, in a sense, obsolete before it was released. I considered dropping all my recent work and using the bleeding edge new official architecture suggestions from Google. My thoughts though were that it was yet untried, and other Android programmers felt the same.
In addition to a more explicit Google-blessed architectural model, I decided to make the app more in line with what most Android shops were doing. Although the Android Universal Image Loader library has worked well for me, it has not been updated at all in eighteen months, a long time in an Android environment where new Android versions are coming out regularly. I switched to the Glide library, as it is popular and people like it. I could just as easily have picked other popular Android image loading libraries such as Fresco or Picasso, but Glide suited my needs better.
I also changed other things. GridView went out, RecyclerView came in. I used Retrofit for JSON loading, and GSON to convert the JSON into objects.
A few things prompted these changes. One was that my method of dealing with my main data structures was not so great. Particularly in giving access to the data model all around the app. I had known that my existing methodology was problematic - but it did work.
However more of the newest Android devices (Nougat) were coming online. With my number of wallpapers growing, as well as Nougat's new constraints, I began seeing TransactionTooLarge exceptions when people scrolled down to the bottom of what were now over 1300 wallpapers.
Another reason for the major refactor is just that I had been working with MVP architecture, Recyclerview etc. in other apps and wanted to bring all of that good stuff into this app.
Any how, here is my timeline of work. As I said in previous blog posts, this is to give people some idea of what goes into programming an Android app.
From December 24, 2016 to March 30, 2017, I am just doing regular updates. From April 23rd, 2017 to now, I am redoing the app in the MVP architecture, as well as making other large changes.
December 24, 2016
These apps are fairly dependent on network connectivity. If the network is not connected, I pop up a dialog fragment. But it pops up while the activity is finishing, which it should not do. So I patch that.
January 13-16, 2017
The images I have on my categories page are larger in file size than they need to be. I shrink them down to 200px each. Also, I have brought in more wallpapers over the past months, and choose better examples than existing to illustrate each category.
March 15, 2017
My big problem on the first release is when I went out to test it and realized it did not work on Marshmallow phones due to Marshmallow's new permissions model. I had done a kludge fix for that ten months before. In February 2017 I bought a Pixel phone running Nougat. While using my app on it and doing some informal QA, I notice there is a race condition in the Marshmallow permissions code, so that it does not always take effect. So I patch that. This is why it's good to have access to a lot of devices for Android. I upload the new version with this fix to Play, which is my last app update on Play for three months (but not my last update to the app, as I am putting about three new wallpapers a day online behind the API accessible to the app).
March 30, 2017
The aforementioned network disconnected dialog fragment is being activated while the onSaveInstance method is running, which should not happen. So I disable that as well and patch it. As it happens rarely, I don't update the new code to Play. One reason is I don't release the fix is I didn't anticipate that I would still be working on the next release all the way into June. So I thought the fix would go in earlier. It is not as major as the Marshmallow fix any how.
April 15, 2017
I go down a blind alley. I try to do a kludge to fix the TransactionTooLargeException that Nougat devices are seeing. But it is not possible - some work will be needed. And since some work is needed, I might as well do it right, and do as much work as is needed.
April 23, 2017
This is the start of work that will not be published on Play until June 12th. I decide to model the app on state of the art Android architecture for Model-View-Presenter.
The sample app for it is on Github. The main documentation page has a paragraph which is very confusing, until I realize that it contains a typo, which I send a pull request to fix. This is not an encouraging start.
May 2, 2017
GridView out, RecyclerView in. Wallpapers are now over 1000, so we need to start recycling views better if users want to scroll down into infinity.
Also, Android Universal Image Loader always served me well, but it has not been updated for eighteen months, and image libraries like Picasso, Fresco and Glide are what the majority of shops are using now. I choose Glide, which has been a suitable choice so far.
May 3, 2017
I start working on the Presenter part of the Model-View-Presenter. I get how this works - the View Fragment and the Presenter both implement off of a contract interface. This way, transactions between the View and the Presenter are made very clear (and testable).
May 6, 2017
I use Retrofit to grab the JSON, and GSON to turn the JSON into POJOs. Retrofit has a ready-made GSON converter. This all makes the code cleaner.
May 13, 2017
I fiddle with Glide's disk caching strategy, so that thumbnail images will tend to only have to be downloaded from the server once.
May 18, 2017
Glide has lots of animations, plus the Recylerview blinks when the data set is changed. I work to minimize this. This is still not totally done, as I have not taken advantage of ranged data notifications to the adapter yet.
June 3, 2017
Trouble with FragmentPagerAdapter. Sometimes a new Fragment is created for an existing tab, whereas the old one comes back to life as well. I start dealing with this. I still don't feel it is totally dealt with, although I can not see any problems it is causing now. I try lots of things with retained fragments, FragmentStatePagerAdapter etc.
June 7, 2017
The code from April 23rd to June 3rd was getting a little convoluted, so this refactoring gets a refactoring. I try to take out all the little kludges to get things working and streamline things sensibly.
June 9, 2017
A real breakthrough. The main data structures are resident in the Model repository. When the app starts up, I pull a reference down to the View's adapters of the relevant needed data structures that reside in the Model repository. That is the first and last time the data structures are referenced - on subsequent updates, all the actual work is done in the Model repository, and the current View adapter is just sent calls that do a notifyDataSetChanged. Very clean (cleaner yet would be ranged notify to the adapter).
June 12, 2017
The todo list is getting shorter and shorter. I decide to punt on ranged notifies, even though it causes the grid to blink on data notifies, particularly on my oldest Android device.
I want the app to open, to do a small JSON grab of the most recent wallpapers, and for those images to be put in Glide and loaded. I want the user to quickly see something. That is the priority, everything else follows. So the first JSON load does not send an app InstanceID to the server. Previously the order had been first JSON pull -> load images -> load InstanceID -> do second JSON pull. Now I do the first JSON pull, and kick off a Runnable to send the InstanceID to the Model repository. Retrofit grabs JSON without InstanceID's until it loads. So the user is not inconvenienced. It works out well. The app is architected well enough, and with clean enough code that these little extras don't really affect things. Timely InstanceID's are nice to have, but not critical.
Why do I send InstanceID's to the server? Because it helps with bug tracking. Some users are having a problem, but tracking by IP does not cut it as their ID's change a lot. Even tracking by device does not cut it as some devices are fairly common. If I get a low rating on Play on a certain day, I look through the server logs for that device type, country etc. Two people who gave me a one rating loaded the JSONs, but no wallpapers, which helped me track down a bug.
So we're coming into the home stretch. I do a production build. Oops. Guava and Firebase libraries conflict. That's simple enough, I don't have much Guava code in the app. I rip the small amount of Guava code I have out.
I QA on my devices. I should probably do more QA, but it's been three months and I am antsy, and this can go on forever. I thought I was releasing a few weeks ago, but QAing kept catching problems. So I release to alpha, and then beta. One of my beta testers says it is all good. The Google Play developer console automatic tests go through fine. So I release to 25% of my users. Later on, I go out. I check the app on a public wifi network which is flaky. Oops, my detail page is messed up. The detail image appears, then disappears. Sometimes it is replaced by a better image a few seconds later, sometimes it is not replaced at all.
June 13, 2017
I fix the error. The higher resolution thumbnail is loaded to a file by Glide, and when that is all copacetic, it refreshes the existing lower resolution (RecyclerView grid) thumbnail, with the existing ImageView's Drawable serving as Glide's placeholder. This seems to work. I QA it for a bit and then release - to 50% of the app users.
There is still more QA I want to do. My main thing right now is that the app is functioning properly. I am most concerned with how the logic is dealing with network latency and flaky networks. As well as other problems that might crop up. Once I feel the app is mostly stable, I can concentrate on other enhancements. Of course, a regular addition of new wallpapers will go alongside this.
It's been a year and three months, and I haven't really promoted the app heavily. It has over 3500 active users, and a 4.3 rating, but I am concerned with the users which give it ratings from 1 to 3. I am concerned with bugs like TransactionTooLargeException. I am concerned with users who have more latency than I deal with (I do some things in QA to test this, but can do more). On top of these stability questions, the app only has 1371 wallpapers, whereas the main competitors have many more. If the app is stable, I can concentrate primarily on adding more wallpapers. After enough wallpapers are added, it would make sense to put in search functionality, which is the main emergent feature which the app could use.
So I will see how this update fares, perhaps do some more small fixes, and if things are going well, may start ramping up the marketing budget some. I previously was targeting English speaking countries and Spanish speaking countries. Currently I am targeting French speaking areas, and will soon be switching to primarily targeting German speaking areas. If everything is stable, and a few new fixes go in, I may ramp up marketing efforts, even if it is just to see how users respond to the app.
I should mention in closing that in April, another effort went into the app framework. I paid someone else to pick three wallpapers a day throughout April. I also paid a Python programmer to speed up the process I had to thumbnail images I had selected. Both people did a good job, and I may work with both again.