As I noted in a earlier post, I received a email for upgrading applications to Google Analytics v4; in that post I upgraded my Xamarin app. On a different Android native app, it also needs to be upgraded for the API changes, but has very little users, so I did upgrade anyway from Firebase analytics to Google Analytics. I’ve not done much work in that app in the last few years and unfortunately the app uses AsyncTasks to do most of the network activities and that was deprecated in API 30 (as of this writing the most recent API is 32).
Looking at the existing code base with that deprecation, essentially the entire app needs to be restructured / refactored in order to use the ViewModel / LiveData and other items on Android app architecture. Instead of what the app did in API 29 and before with AsyncTasks and EventBus usage to try break apart the intermixing of UI interaction and data within the Activity / Fragment; the recommended Android architectural guideline is:
- UI layer
- Domain layer (optional)
- Data layer
Before starting the restructuring, I upgraded the Java library that contained all of the classes and the network interfaces: updating from Retrofit to Retrofit2, reviewing and upgrading the interface calls and classes as well as upgrading the project library to the current version of Android Studio / Gradle. As part of that I moved some of the implementation out of the application into that library, so that I’d not have to worry about the performing the actions needed to retrieve the data. I’ll be using the login implementation from this library.
For tasks like this, I normally break them down into smaller pieces to make it easier to design and experiment with instead of working on all of the problem at the same time. As the first thing in the app that the user can do is log in, it’s a logical place to start.
Time to log in
I thought about how to show this process, so I have made different releases on Github and will go in that order in this article so you can follow along. This will be done using Java. I did find several login examples out there, one in Kotlin, one in pre APK 30 Java, and a site that links to many other code login articles (a nice reference).
Release 0
To start off it’s pretty easy: new -> new project -> Login Activity. I went and created a Github release from what Android Studio scaffolded.
Release 1
I wanted to set up the real release for success, so time to get the easy tasks out of the way. The original scaffolded project has the one Activity that exits at the end – good bad or indifferent, so I wanted it to load another Activity with a successful login and leave it running on a unsuccessful login. I’ve also added an Application subclass as well and added the Internet permission to the manifest. This is the release that I’ll build upon to actually perform the login.
From here, need to add a way to perform the network calls in the background, so in the updated Application subclass, I’ve added a Executor, which the will perform the network call on a non-UI thread; also created an instance of the class that holds the network interface.
Release 2
The scaffolded project has a Login Repository. The main business change in here was the logged in return result didn’t contain the logged in session token; I get that they didn’t need it with faking out the login, but they should have known something would come back from a log in method and incorporated that in the scaffolded project flow. To actually store the logged in session token, we will need the Context from the Activity. To get that Context to be able to save, the flow will go from the login data store login result -> login repository -> view model -> userid / password Activity. There’s some processing done in the Activity and then sends the session token and the context -> view model -> login repository -> session data source stores the session.
Now if we stored the subclassed Application Context in the view model, we wouldn’t need to do this, we could have just sent the context from the view model back into the login repository with the save session token method and the login data source would have stored the session. That said, it would have introduced a Android component into the view model, which should be avoided. As it wasn’t too complex to jump back to the Activity and make another round trip; but if needed, it is possible to extend a view model from AndroidViewModel instead of from ViewModel which would allow Context storage in the view model. An interesting quote in a article in relation to data storage:
If we think about it, shared preference is not the first thing that we associate with the data source, but it is as much as a data source as anything else and should be handled the same way. Any transformation of data should be avoided in this layer and it should purely be data retrieval at this layer.
https://medium.com/swlh/defining-data-sources-in-clean-architecture-android-f0ee7cbc6634
The session token will be stored in a SharedPreferences; this will be addressed later in release 3. The user / password acceptance Activity is updated to take this into account. It’s view model is updated with the majority of the changes occurring in how to deal with passing the error message back to the user.
In the new Activity we added in the prior branch is updated, adding a view model, so that it can get the session token and display it to the user; which will prove the storage and retrieval of the token works. Lastly we update the XML’s from the scaffolded text in order to show the correct login field names.
That was a lot of work, here’s release 2 on Github.
Release 3
This release will: clean up the generic classes, move the SharedPreferences apply’s off the UI thread and then swap the SharedPreferences for DataStorage Preferences, part of Android Jetpack. In this particular case, the use of DataStorage Proto doesn’t seem worth the setup effort, as the login returning data is a session token string. As it was difficult to find the Android DataStorage pages, here’s a link dump: Jetpack guide (with code), announcement background, official release [2022-03-14], and a Android codelab (in Kotlin).
After reviewing the really unhelpful warning in the subclassed Application class, I found out that the isNetworkAvailable() method contained classes that were deprecated within it and as it wasn’t used, I deleted it.
As noted in the DataStore announcement background, SharedPreferences shouldn’t be used on a UI thread as it can cause ANR’s. Because of this, I ended up changing the two applies (set / delete) to create a runnable and put it on an ExecutorService thread to move it off of the UI thread. I renamed this class from SessionDataSource to SessionPreferenceDataSource. It’s possible that a SharedPreferences read might also cause a ANR, however with only the session token being put into the SharedPreferences, and it’s relatively small size, it seems a negligible risk and I left it alone. I also implemented an method interface so that either the SharedPreferences or new DataStore could be interchangeably with only the change needed would occur in the Activity’s view model factory classes.
There aren’t too many examples in Java for the DataStorage use. The exception is this really useful article, which I recommend. There is also a video on DataStorage Java; but with my testing with the 1.0.0 implementations, I found was the read didn’t work correctly across Activities; so I ended up not using it in this repo. As an aside the Java implementation of DataStorage requires RxJava usage. I’m not really too familiar with it, but I did view this video and from an introduction standpoint was good. The required additions to the gradle file are here.
Unfortunately, the DataStorage method also requires an Android Context as the SharedPreferences does; it would have been nice to fully unplug from the Android architecture. Unlike the SharedPreferences, there’s no way to remove an Preference item, so i set the currently unused delete session method to throw an exception.
The helper class that has all of the methods that the SessionDataStoreDataSource uses is named DataStorePreferenceGenericified. It has two main methods in it: put and get. Put essentially an asynchronous update; get Preference call is also asynchronous and it generates a RxJava flowable, so with the use of RxJava’s blockingFirst, the result can be retrieved. If nothing appears, it will throw a NoSuchElementException exception which will be caught and ignored. I’ve reviewed the instrumentation time of the DataStore get method and after the blockingFirst was passed the time either showed the same as the start of async read or a millisecond or two after it. It was pretty much the same as the SharedPreference timing.
The Android native logging in restructuring demo is complete. Time to start refactoring another part: restructuring part 2 – Kotlin networking.
Hopefully this gave you some insight into what and how I went about dealing with logging under the new architecture model.