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

  1. Remote Configuration
  2. Dynamic Delivery
  3. WebView
  4. Feature Flags
  5. Content Management System (CMS) Integration
  6. Push Notifications
  7. 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

Popular posts from this blog

Revolutionizing Mobile App Development with AI: The Power of Variability and Maximizing Efficiency

MobileFirst, MobileLast, or MobileOnly: Navigating the Mobile Landscape in 2023