Simplifying Mobile App Updates: Bypassing the App Store
Submitting an app update to the app store can take time and effort. Developers must adhere to stringent guidelines, await approval, and navigate the potential pitfalls of rejection. Additionally, even after an update is successfully published, users may ignore or miss it, depriving them of critical improvements and new features.
Fortunately, there are several strategies developers can use to update a mobile app without requiring a trip to the app store. These methods streamline the update process and ensure users can access the latest features and fixes immediately. Here, we outline seven options and delve into three more complex but powerful methods.
Options for Updating Mobile Apps Without the App Store
- Remote Configuration
- Dynamic Delivery
- WebView
- Feature Flags
- Content Management System (CMS) Integration
- Push Notifications
- In-App Updates
An in-depth look at Remote Configuration, Dynamic Delivery, and Feature Flags
1. Remote Configuration
Overview: Remote Configuration allows you to modify app behavior and appearance without publishing an update. It stores configuration settings on a remote server and fetches them periodically or at launch.
Tooling Choices:
- Firebase Remote Config
- Azure App Configuration
- AWS AppConfig
Pros:
- Immediate effect: Changes are applied when the new configuration is fetched.
- You don't need to submit an app store: The changes are managed remotely.
- Flexibility: Suitable for various changes, from UI tweaks to feature toggles.
Cons:
- Network dependency: Requires internet access to fetch the latest configuration.
- Latency: There may be a slight delay in fetching and applying new settings.
Examples:
iOS Example Using Firebase Remote Config:
swift
import UIKit
import Firebase
class ViewController: UIViewController {
var remoteConfig: RemoteConfig!
override func viewDidLoad() {
super.viewDidLoad()
// Initialize Remote Config
remoteConfig = RemoteConfig.remoteConfig()
// Set default values
let remoteConfigDefaults: [String: NSObject] = [
"theme_color": "#FFFFFF" as NSObject // Default color: white
]
remoteConfig.setDefaults(remoteConfigDefaults)
// Fetch remote config values
fetchRemoteConfig()
}
func fetchRemoteConfig() {
remoteConfig.fetch(withExpirationDuration: 3600) { status, error in
if status == .success {
self.remoteConfig.activate { changed, error in
self.applyThemeColor()
}
} else {
print("Config fetch failed: \(error?.localizedDescription ?? "No error available.")")
}
}
}
func applyThemeColor() {
let themeColorHex = remoteConfig["theme_color"].stringValue ?? "#FFFFFF"
view.backgroundColor = UIColor(hexString: themeColorHex)
}
}
extension UIColor {
convenience init(hexString: String) {
let hex = hexString.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
var int = UInt64()
Scanner(string: hex).scanHexInt64(&int)
let a, r, g, b: UInt64
switch hex.count {
case 3:
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6:
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8:
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(a, r, g, b) = (255, 0, 0, 0)
}
self.init(red: CGFloat(r) / 255, green: CGFloat(g) / 255, blue: CGFloat(b) / 255, alpha: CGFloat(a) / 255)
}
}
Android Example Using Firebase Remote Config:
java
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import com.google.firebase.remoteconfig.FirebaseRemoteConfig;
import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings;
public class MainActivity extends AppCompatActivity {
private FirebaseRemoteConfig mFirebaseRemoteConfig;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Initialize Remote Config
mFirebaseRemoteConfig = FirebaseRemoteConfig.getInstance();
// Set default values
mFirebaseRemoteConfig.setDefaultsAsync(R.xml.remote_config_defaults);
// Fetch remote config values
fetchRemoteConfig();
}
private void fetchRemoteConfig() {
FirebaseRemoteConfigSettings configSettings = new FirebaseRemoteConfigSettings.Builder()
.setMinimumFetchIntervalInSeconds(3600)
.build();
mFirebaseRemoteConfig.setConfigSettingsAsync(configSettings);
mFirebaseRemoteConfig.fetchAndActivate()
.addOnCompleteListener(this,
task -> {
if (task.isSuccessful()) {
applyThemeColor();
} else {
// Handle the error
}
});
}
private void applyThemeColor() {
String themeColor = mFirebaseRemoteConfig.getString("theme_color");
View rootView = findViewById(R.id.root_view);
rootView.setBackgroundColor(Color.parseColor(themeColor));
}
}
2. Dynamic Delivery
Overview : Dynamic Delivery (Android) and On-Demand Resources (iOS) allow apps to be split into smaller modules, which can be downloaded as needed. This keeps the initial app size small and improves user experience.
Tooling Choices :
- Android Dynamic Delivery
- iOS On-Demand Resources
Pros:
- Reduced initial download size: Users download only the core app initially.
- On-demand features: Modules are downloaded as needed, saving storage space.
- Efficient updates: Only specific modules can be updated without a full app update.
Cons:
- Complexity: Requires careful planning and implementation.
- Dependency on the network: Users need internet access to download modules.
Examples:
Android Example Using Dynamic Delivery:
java
import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
import com.google.android.play.core.splitinstall.SplitInstallManager;
import com.google.android.play.core.splitinstall.SplitInstallManagerFactory;
import com.google.android.play.core.splitinstall.SplitInstallRequest;
import com.google.android.play.core.splitinstall.SplitInstallSessionState;
import com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener;
import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus;
public class MainActivity extends AppCompatActivity {
private SplitInstallManager splitInstallManager;
private SplitInstallStateUpdatedListener listener;
private int sessionId = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
splitInstallManager = SplitInstallManagerFactory.create(this);
listener = new SplitInstallStateUpdatedListener() {
@Override
public void onStateUpdate(SplitInstallSessionState state) {
if (state.sessionId() == sessionId) {
switch (state.status()) {
case SplitInstallSessionStatus.DOWNLOADING:
// Show download progress
long totalBytes = state.totalBytesToDownload();
long downloadedBytes = state.bytesDownloaded();
Log.d("MainActivity", "Downloading: " + downloadedBytes + "/" + totalBytes);
break;
case SplitInstallSessionStatus.INSTALLED:
// Module installed successfully
loadAndLaunchModule();
break;
case SplitInstallSessionStatus.FAILED:
// Handle the error
Log.e("MainActivity", "Installation failed: " + state.errorCode());
break;
// Add more cases to handle other states
}
}
}
};
splitInstallManager.registerListener(listener);
}
private void requestFeatureModule() {
SplitInstallRequest request = SplitInstallRequest.newBuilder()
.addModule("your_dynamic_feature")
.build();
splitInstallManager.startInstall(request)
.addOnSuccessListener(sessionId -> this.sessionId = sessionId)
.addOnFailureListener(exception -> Log.e("MainActivity", "Installation failed", exception));
}
private void loadAndLaunchModule() {
// Code to load and launch the dynamic feature module
}
@Override
protected void onDestroy() {
splitInstallManager.unregisterListener(listener);
super.onDestroy();
}
}
iOS Example Using On-Demand Resources:
swift
import UIKit
class ViewController: UIViewController {
var resourceRequest: NSBundleResourceRequest?
override func viewDidLoad() {
super.viewDidLoad()
// Request resources associated with the
"level1" tag
resourceRequest = NSBundleResourceRequest(tags: ["level1"])
resourceRequest?.beginAccessingResources { error in
if let error = error {
print("Error: \(error.localizedDescription)")
} else {
self.loadLevel1Assets()
}
}
}
func loadLevel1Assets() {
// Load and use the resources
}
}
4. Feature Flags
Overview: Feature Flags enable you to turn features on or off remotely for different user segments. This is useful for gradual rollouts, A/B testing, and instant feature toggling.
Tooling Choices:
- Firebase Remote Config
- LaunchDarkly
- Split.io
Pros:
- Granular control: Enable or turn off features for specific user groups.
- Safe rollouts: Gradually introduce new features and monitor their impact.
- Flexibility: Instant updates without redeploying the app.
Cons:
- Management overhead: Requires a system to manage and track feature flags.
- Potential for misuse: Over-reliance on flags can lead to code complexity.
Examples:
iOS Example Using Firebase Remote Config:
swift
import UIKit
import Firebase
class ViewController: UIViewController {
var remoteConfig: RemoteConfig!
override func viewDidLoad() {
super.viewDidLoad()
// Initialize Remote Config
remoteConfig = RemoteConfig.remoteConfig()
// Set default values
let remoteConfigDefaults: [String: NSObject] = [
"new_feature_enabled": false as NSObject
]
remoteConfig.setDefaults(remoteConfigDefaults)
// Fetch remote config values
fetchRemoteConfig()
}
func fetchRemoteConfig() {
remoteConfig.fetch(withExpirationDuration: 3600) { status, error in
if status == .success {
self.remoteConfig.activate { changed, error in
self.applyFeatureFlag()
}
} else {
print("Config fetch failed: \(error?.localizedDescription ?? "No error available.")")
}
}
}
func applyFeatureFlag() {
let newFeatureEnabled = remoteConfig["new_feature_enabled"].boolValue
if newFeatureEnabled {
// Enable the new feature
} else {
// Keep the old feature
}
}
}
Android Example Using Firebase Remote Config:
java
import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
import com.google.firebase.remoteconfig.FirebaseRemoteConfig;
import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings;
public class MainActivity extends AppCompatActivity {
private FirebaseRemoteConfig mFirebaseRemoteConfig;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Initialize Remote Config
mFirebaseRemoteConfig = FirebaseRemoteConfig.getInstance();
// Set default values
mFirebaseRemoteConfig.setDefaultsAsync(R.xml.remote_config_defaults);
// Fetch remote config values
fetchRemoteConfig();
}
private void fetchRemoteConfig() {
FirebaseRemoteConfigSettings configSettings = new FirebaseRemoteConfigSettings.Builder()
.setMinimumFetchIntervalInSeconds(3600)
.build();
mFirebaseRemoteConfig.setConfigSettingsAsync(configSettings);
mFirebaseRemoteConfig.fetchAndActivate()
.addOnCompleteListener(this, task -> {
if (task.isSuccessful()) {
applyFeatureFlag();
} else {
// Handle the error
}
});
}
private void applyFeatureFlag() {
boolean newFeatureEnabled = mFirebaseRemoteConfig.getBoolean("new_feature_enabled");
if (newFeatureEnabled) {
// Enable the new feature
} else {
// Keep the old feature
}
}
}
Conclusion
Updating mobile apps without the app store can significantly improve the user experience by ensuring timely updates and reducing the hassle of app store submissions. Remote Configuration, Dynamic Delivery, and Feature Flags are powerful tools that allow for dynamic, flexible, and efficient app management. By leveraging these techniques, developers can keep their apps up-to-date and responsive to user needs without the delays associated with traditional app store updates.
Comments
Post a Comment