Following yesterday’s post on repacking an Android APK, I was pointed to an awesome post by Daniel Causer entitled Build Once Release Everywhere – APK. Daniel’s post goes into a lot of detail on how to setup your build and release process. Specifically the scripts that he’s created for automating the repacking process I described in my post (you can grab them from his github repo.
I think the logical next step is for the creation of a Azure DevOps extension that either allows you to invoke apktool from a task in the build or release pipeline; or an extension that does the whole extract; replace/rename/modify file and then repack and sign. I suspect the former might be the best way as it would give developers the most flexibility. However, it would require more steps to be configured in the release pipeline.
In my previous post talking about targeting different environments I ended with the proposition that what we need to be able to do as part of the release pipeline for an app is to adjust the configuration file that’s included in the app package. In this post I’m going to manually walk through what this process would look like for an Android APK (and yes, before you all jump up and down and say that I should be using an app bundle, I’m aware of this but let’s do this process step by step).
Ok to begin, what I need is a release-ready APK and for the purpose of this post I’m going to use a Flutter app. The process I’m going to describe will work for any Android APK regardless of the toolchain/framework/technology set that you’re using to build your app. The only difference with say a Xamarin.Forms application would be how you package the configuration file; the code you’d need to write to read the configuration file and of course the XAML for displaying the contents to the screen.
Basic App Structure
My sample Flutter app starts with the default app template you get when creating a new Flutter app in VS Code. I then added a single text file, config.txt, to the assets folder; included the file in the pubspec.yaml and then adjusted the _MyHomePageState class to load the contents of the file (a basic walk through of reading files that are included in the app package are included in this post).
The contents of the config.txt file simply says “***Default App Config***” and running the app looks like:
Release Ready APK
In order to walk through the process of repackaging an APK, I firstly need to make sure I have a release-ready APK. By this I mean that I have an APK that’s been built in release mode and that has been signed, as if I were going to submit it to the Google Play store.
The Flutter documentation has very clear instructions on how to package and sign your application. I followed these instructions to generate a keystore that is used as part of the Flutter build process to sign the application.
In order to test to make sure your APK is good to go, simply copy it to a real device and test that you can install and run it. For this I simply uploaded the APK to dropbox and then opened the file on my device. You could also attach to an email or even side load directly via USB cable if you choose.
Repackaging to Change App Configuration
The basic process we want to follow is:
Unpack the APK
Modify the config.txt file
Repack the APK
Sign the APK
Unpacking the APK
In order for us to be able to modify the config.txt file that’s packaged in the app, we first need to unpack the APK. For this I’m going to use the APKTool utility. Follow the installation instructions to make sure you have the latest version and the appropriate directories added to the PATH variable (you may need to add the Java directory to your PATH).
Once installed, to unpack an APK you can simply call the APKTool with the decode, or just “d”, argument:
apktool d app-release.apk -o extracted_apk
Note that I specified the “-o” argument to allow me to specify the output folder.
Modify the Config.txt
In this example we’re simply going to modify the config.txt file that we have included in the app package. You could be more fancy and include a json or xml configuration file but essentially all you’re going to do in this step is modify the contents, or replace the file entirely.
In the case of my sample Flutter app, the location of the config.txt is in the sub-folder “\assets\flutter_assets\assets”. If you’re application is built using Xamarin.Forms, your configuration file may be located in a different folder – you just need to search the extracted folder and locate the file.
I’ve changed the contents of the file to “***Modifited App Config***”
Repack the APK
After making the change to the config.txt file we then need to repack the APK. For this we can again use the APKTool, this time specifying the build, or “d”, argument:
The last step is to sign the APK and for this I’m going to use the Uber Apk Signer along with the keystore I setup as part of configuring the Flutter release build.
java -jar uber-apk-signer-1.1.0.jar -a app-release-mod.apk --ks c:\<<path to keystore>>\key.jks --ksAlias key --ksKeyPass <<password>> --ksPass <<password>> -o app-release-mod-signed
And that’s it – if we check in the app-release-mod-signed folder there’ll be a new APK that’s signed and ready to go.
Copy the application to your device and run it and you’ll see the updated configuration value (and yes, complete with spelling mistake!!)
Repackaging Apps During Release Process
As you can see from this process, it’s not too hard to adjust a configuration value by repacking an Android APK. You can simply include the APKTool and the Uber-Apk-Signer alongside your application and script out these steps as part of your Release pipeline.
I’ve posted previously on using visual states in Uno and how they can be used to effectively manage the different visual layouts a page can take on. These may be changes in layout due to the application being resized, or perhaps due to different data loading states. I’ve recently created a Uno build of the BuildIt.States library, BuildIt.States.Uno. In this post I’m going to walk through using this library to help manage visual states from within your view model.
One of the topics that’s quite hot at the moment is whether XAML has had its day and whether the new coded UI techniques provide a better solution. I’m not going to go too far into this debate but one of the things I really like about XAML is the separation of the UI (declared in XAML) from its data representation (the ViewModel).
Whilst the data binding framework of both UWP and Xamarin.Forms works well for connecting properties on a ViewModel to attributes of a UI element, there is no way to connect visual states on the page, with some aspect of the ViewModel. This is where the BuildIt.States library kicks in.
Thinking about Visual States
Before we get into using the BuildIt.States library, lets start by thinking through the different states that our page can go through. For the purpose of this post we’re going to build a simple app with the following spec:
The app has a single page that has a button, “Load Data”
When the button is pressed, the button will be hidden and a loading indicator will be shown.
The app will attempt to load some data.
Loading data will randomly succeed or fail
When loading is complete (either succeed or fail) the loading indicator will be hidden, a message will be displayed indicating success or failure, and the Load Data button will be displayed again.
If the Load Data button is pressed again, the status of the previous attempt will remain on screen until the new attempt to load data has been completed.
An initial read of this spec would seem to indicate that there is a single set of states:
Loaded – Success
Loaded – Failed
However, when the button is pressed for the second time, there are two more states that need to be included : Loading (Loaded – Success) and Loading (Loaded – Failed).
An alternative way of thinking about the states is that there is a group of states that pertain to whether data is being loaded, and a different group of states that pertain to whether the page has data:
These states control whether the button or the loading indicator is visible
These states control the visibility of the data, or in this case the message indicating success or failure of the data loading.
Visual States in XAML
Since the focus of this post isn’t on how to design a page in XAML I’ll skip over the steps involved in laying out the page. In summary, after creating a new Uno project (using the Uno project templates), I opened the MainPage.xaml in Visual Studio Blend. Whilst Blend is a long way from being the design-first tool it was once envisaged as, it does still have support for defining visual states, which is sorely missing in Visual Studio (and probably the only reason I still use Blend).
Using the design surface in Blend I’m able to layout the various TextBlock, Button and ProgressRing to build the simple UI for the app. I then use the States tool window to create two Visual State Groups (LoadingStates and DataStates) and the associated Visual States for showing the appropriate elements. The resulting XAML looks like the following:
Clearly this code isn’t production ready (string literals, codebehind, no error handling etc) but when I run the app, I can see the desired state changes. Here I’m just showing Android but since it’s Uno, it should work nicely on iOS, UWP and WASM too.
Now that we’ve defined the visual states, we need a way to both control and track the states in our ViewModel. In the same way that we can use data binding to update attributes of the visual elements on the page, we need a way to mirror visual states within our ViewModel. This is where we can make use of the StateManger from the BuildIt.States library.
In the following MainViewModel, a StateManager is created and setup using two different state groups. Rather than using string literals, we use an enum to define each state group. Note that each enum has a Base value, which reflects the default, or unset, state.
I also need to update the codebehind in MainPage to create an instance of MainViewModel and then invoke the LoadData method when the Load Data button is clicked:
publicsealedpartialclassMainPage : Page
public MainViewModel ViewModel => DataContext as MainViewModel;
DataContext = new MainViewModel();
privateasyncvoidLoadDataClick(object sender, RoutedEventArgs e)
Connecting ViewModel to Visual States
Of course, if we run the code at this point, the state tracked within the MainViewModel don’t update the UI. To complete the loop, we need to attach another StateManager to the visual states defined on MainPage, and then bind the two StateManagers so that they can remain in sync. This is all done in the MainPage codebehind.
And there we go – now when you run the application and press the Load Data button the LoadData method on the MainViewModel will be invoked. As the states of the MainViewModel change, the StateManager and subsequently the visual states on the MainPage are updated.
Whether you’re developing an Android app in Kotlin, a cross-platform app in Flutter or Xamarin Forms, or an Xbox app in C#/XAML, supporting multiple environments when building an app, is just not as easy as it should be. For example the different environments might be dev, test, staging, prodution etc to align with your dev, test and release process. Alternatively, you might have a white-labelled app that you can configure for a particular customer by adjusting some application settings. In each of these scenarios, it would be ideal to be able to deploy our application, along with a configuration, or settings, file. In this post we’re going to discuss why this isn’t possible, how this problem is typically solved, and then discuss an alternative approach to solving the problem.
Before we jump in and discuss native applications, let’s take a look at a couple of scenarios where configuration files are already supported. The first example is a typical web applications that can be built once, and then released to an almost unlimited number of environments, where each one can have different settings, or attributes, applied using some form of a settings or configuration file. For example with an ASP.NET application you can specify application settings in the web.config or app.settings files. Alternatively if you’re deploying to an Azure App Service, you can configure various settings directly via the Azure portal (including overriding settings on a per-slot basis).
The use of configuration files isn’t isolated to web applications. In fact both WinForms and WPF applications can take advantage of the ConfigurationManager class in the .Net Framework to dynamically load configuration data from a file packaged alongside the application.
The introduction of application stores (eg the iOS App Store, Google Play Store and the Microsoft Store) brought with it the notion of an application package. Applications were packaged and then signed to ensure that what was received, and subsequently installed, on the device was the same package the publisher had submitted and that had been approved for distribution. None of the main stores support distributing a configuration file alongside the application, in the same way you could have done with a private distribution of a WinForms app.
Packaged Configuration Files
Given that it’s not possible to distribute a configuration file in parallel to the application, it is necessary to include configuration files within the application package. There are a couple of alternatives that you should consider when deciding on a configuration system.
Build Configuration Constants
This post by Jon that provides some background on what a build configuration is within Visual Studio and how to take advantage of it to control the behaviour of your application during development (Debug configuration) and in production (Release configuration).
Build configurations can define compilation constants that can be used to dynamically include or exclude code at compile time. The Debug build configuration typically already has the DEBUG constant defined but you can define your own. For example in the following image the DEV_ENV constant has been defined for the Debug build configuration.
In code, you can then use these constants to determine what code gets compiled. For example in the following code, when compiled with the Debug build configuration the DEV_ENV constant is defined, so the first definition of HelloText will be compiled. For all other build configurations, the DEV_ENV constant isn’t defined, so the second definition is compiled.
#if DEV_ENVpublicconststring HelloText = "Hello World - Dev Environment";
#elsepublicconststring HelloText = "Hello World";
You can extend this to include or exclude entire files by modifying the project file. There is no UI built into Visual Studio for doing this but the syntax of the csproj project file is relative simple, so not too hard to tweak. The following example demonstrates how to exclude two files (since all files are include by default within the project folder system), DebugConstants.cs and ReleaseConstants.cs, and then to selectively include them for the different build configurations.
As you switch between Debug and Release build configurations in Visual Studio you can actually see the change in the Solution Explorer, showing which files will be included. In the following image the left screenshot of the Solution Explorer window shows that the DebugConstants.cs file has been included in the Debug configuration, whilst the right shows the ReleaseConstants.cs is included for the Release configuration.
The app.settings file can be replaced during the build process in order to switch between different environments. You can either choose to replace the entire app.settings file, or you can simply substitute individual key-value pairs.
Mobile Build Tools
Dan Siegel (of Prism notoriety) has developed some mobile build tools that he’s been working on to make it easier for developers to setup DevOps for mobile applications. I’d highly recommend integrating these tools into your build pipeline.
Build v Release Tasks for Multiple Environments
Ok, so before I wrap up this post I want to go back to the original premise I discussed. What I want to be able to do is to build my application once and then have different configurations for each environment. We can think of the devops for our application in two stages, Build and Release. The Build part of our process should do just that, it should build our application, and it should only have to build it once irrespective of what environment it’s going to target. The Release part of our process should augment the application configuration so that it targets the different environment.
The solutions presented so far have all resulted in the need to have different builds setup for each environments, so none of them present an ideal solution. The primary issue with applications is that the packaging format doesn’t support an external configuration file, so it’s not as simple as deploying a web application where you can simply change the configuration file.
To address this issue we need to look at how we can re-package our application during the release process, allowing us to modify a configuration file that’s included as part of the application package. More on this to come….