Wed, 01 Aug 2018
This is my 6th post in a series about the Wallpapers Android app I developed that is on Google Play. The first blog posts describe how I planned it, started developing it, released it, updated it, and if you want a view of the entire development process of an app, you might want to start reading from the beginning.
This blog post is about the last version I released, where I translate it from Java to Kotlin, and from MVP architecture to MVVM architecture with various Jetpack components. The app is a free one where people can browse through possible wallpapers for their phone background and/or lock screen, and then download and set them.
In my last post I talk about how Google favored Model-View-Presenter architecture in early 2017, and in the midst of my app writing, changed the roadmap at I/O 2017 to Kotlin and (then-beta) new Android architecture components. Any how, I ignored this then and kept plowing ahead with Java and MVP without these new components for this project. I did pick up the Android architecture components (Jetpack) and Kotlin on other projects though. I was busy doing these and other things, and any how, my app stability and functionality was pretty solid any how. Any how it was a little over nine months before I dived back into the app code, rewriting it in Kotlin (and Jetpack).
The Google Android sample apps that use Kotlin and Jetpack components - the sunflower app, the Github app, and particularly Google's Reddit network paging app, were apps I studied as I learned Kotlin and Jetpack. I actually rewrote the networking paging app from the ground up, removing anything extraneous, until it was at under 1000 lines of Kotlin. I also saw how the various classes worked together as I built it.
I used this stripped down networking paging app I culled as the framework of the new Wallpapers app. The existing Java language, MVP architected Wallpapers code had components I sprinkled into this rewritten app which uses Kotlin and Jetpack. It was in June of this year I decided to rewrite the Wallpapers app in Kotlin, using modern Jetpack components like LiveData, ViewModel, Paging and so forth. I want to make the app better and keep it up to date, and I also want to hone my somewhat new Kotlin and Jetpack skills and a slightly more complicated challenge.
June 9, 2018
In Android Studio, create new Android Kotlin app on the basis of my stripped down version of Google's Reddit networking paging app. Create standard Android Kotlin skeleton. Then add ServiceLocator (no Dagger in this app yet), ViewModel, Repository. Then add Glide to load the images. Then add the RecyclerView and ViewHolders. Some of this stuff I had in the old app, which makes things easier.
June 10, 2018
Add a (Room) Dao. Add a (Retrofit) web API. Add more logic to the repository. Load the first url. It loads! Now I need to parse it. Now translate more network and database logic from the old Java MVP app to the new Kotlin MVVM app. I make good use of Android Studio's Java to Kotlin code converter, which does not always work (and even when it does, is not always exact). Work on the ViewHolder. Now load some images. They load! Oops, some of the filenames have UTF-8 and URL encoding quirks. Translate the old code that deals with that to Kotlin.
June 11, 2018
Now redo the frame. Cool, load three images in a row, just like the old app. Add the app icon. Since Android 7.1 round icons have come to the fore, I might have to think about redesigning my icon. OK, a grid of thumbnails is loading, just like the old app. On the old app I could select a thumbnail to see it in more detail, as well as find out more about it, as well as allow me to download it and, if I want to, set it as my wallpaper. So I start working on that detail page in this app. I start with loading the thumbnail from the grid in the new page with Glide.
June 19, 2018
OK now I have a detail page with an initial thumbnailed picture up top. I put in Retrofit calls to get the detail information, and some of the logic to send that state information to the UI.
June 20, 2018
I put in more logic to pull from Retrofit to the UI. I introduce data binding into Gradle, the Activity and the XML. Ah, my Dao SQL calls can be improved. Android really is getting full stack - I can use my server SQL skills locally in the Android app.
June 22, 2018
OK now dealing with asynchronous threads, LiveData etc. The database takes time to get the information, I have a LiveData object in the repository that posts the state information to the ViewModel though, so that the different parts of the app can know when the data is ready.
June 24, 2018
I make the links on the detail page clickable (to web pages). The Wallpaper class which the detail page uses has a number of String variables, and the transformations are a little kludgey, but it works. One of these variables are wallpaper categories which were missing until now, start putting it in.
June 25, 2018
OK - so now we're adding the more complex attributes of the wallpaper. We did the easy ones early, categories was yesterday, now we're adding the wallpaper's licenses. OK cool. OK, now we move on to downloading the wallpapers. Works! OK, now we set the wallpaper as a background. Works (tentatively)!
June 27, 2018
OK, so the other app opens as a three tabs, one selected at a time. So put that in. We have a grid Activity, so make it a Fragment like the other app. OK. So the first two tabs are similar, the third tab is a list of our category types for wallpapers (nature, flowers, cats, space, food etc.) So put that list in.
June 28, 2018
The old app had progressive thumbnail loading - when we go to the detail page we first load the small grid thumbnail, then we load a more detailed thumbnail over it. So we put that in. It seems simpler here, Glide probably improved.
July 1, 2018
We have been loading the recent tab, add a real popular tab which loads the popular wallpapers. I allude to how I choose which wallpapers are popular in an early blog post, although it has been refined since. The method to choose which wallpapers are popular is look through the logs and see what wallpapers have been downloaded by a unique IP, and then scoring them, but using how many days since the download as a score in an exponential decay overall score. It works pretty well, I think it puts me over all the other wallpaper apps actually in that one regard. Perhaps I'll go into more detail in another blog post.
July 2, 2018
OK so now when you click on one of the list of categories, a category page actually loads. The Retrofit logic, Room, ViewModel and UI stuff is there now. Also start sending version and language information to the (test) server, as well as the Instance ID (which we do not initially load in the main thread!)
July 3, 2018
So now the paging library deals with loads. It is a little different from my old hand-tuned code. My old code did a small initial load, so that even on slow connections, something would appear on the screen, and subsequent JSON loads were larger. Also, I did a lot of pre-loading so that scrolling went smoother. One big difference is the Android UI knew how large the grid would be in the old app, and now it does not know until the last page is loaded. So that is a factor in slowing scrolls down.
The old Java code used Java TreeMap to sort the category list for
different languages. I send a "java.util.TreeMap
I put the dev and production URLs for the REST API in a saner, central place. I improve the network error message (in the old app it was a dialog window). I upgrade various libraries.
Upgrade Kotlin 1.2.50 to 1.2.51.
July 7 - 27
OK, I had a list of to-do's, and most of them (except the simple ones to leave to the end, like bump app version number, turn on production URL, put in ads etc.) are done. Two tougher ones remain - making sure I poll the web API periodically, and keeping my place on the grid when I click on a detail and back into the grid (something other example apps like the Google Sunflower app do not do).
So for having a robust scheduled web poll time - I go down some blind alleys, like the way Google's Github app does it. It is not robust enough for me. I also run into all kinds of headaches, like Room deletions are not working for me. They do when I add onDelete CASCADE ForeignKey parameters to various Entities though.
I am storing the last web poll time in Room. Not sure if it is 100% necessary, and it may be overdoing things, but I'd rather know it was being polled then chance it not being polled. Any how, all seems OK but I do not fully understand this and should revisit it later.
The web poll code is done. Yay. Now I get to work on keeping my location in the grid.
Keep location in grid after clicking in detail. I do it essentially the old way I did it before, except the old way I did not do a scroll until the presenter notified the UI that the grid was populated, and now I have an observer in the UI which notifies me when the grid is populated. So it works, yay.
August 1, 2018
OK I started this about two months ago, spent about three weeks (when I had time) on web polling, and then three days on scrollToPosition in the new app. So I'm a little antsy to publish. I QA and QA things and they look OK. I make a release and send to another app, signing both the jar and the whole APK, as I want it to work with old and new devices. Things look OK so I send it up to internal testing on Google Play.
The results come back. A crash. I look. Some of the code I did an automatic Java to Kotlin translation of did not come out exactly right. I do a non-null assertion (!!) where I should not. I had a null check later in the old code, so it was working before. Any how I redo the code, QA, especially in what the changed code deals with, and upload again to Google Play internal testing. Google is still running it through its testing devices.
So we'll see how this goes. The app was fairly solid before in terms of stability. One problem users had sometimes in the old app was with the Environment.getExternalStoragePublicDirectory() call. One problem I had is I knew that call was failing but did not know why. I rolled my own network crash report system and discovered it was usually because the mkdirs() call on the object returned from that call was failing. Which I still have to figure out. Other than that, things were fairly solid.