DEV Community

Cover image for Single Storage, Multiple Flutter Mobile Apps
Adole Samuel
Adole Samuel

Posted on

Single Storage, Multiple Flutter Mobile Apps

This is a long one.

Overview:

What is Single Storage for Multiple Apps?

Single storage for multiple apps refers to a setup where two or more mobile applications on same platform with different bundle ids or package names can access and use the same local data stored on a device, such as user preferences, user data, cached content, authentication tokens.
It basically means multiple apps from the same publisher can share a common source rather than maintain separate storage spaces.

Why is this not the default?

Every app on iOS and Android runs its own sandbox that no other app can access. This improves security, user privacy; your banking credentials can't be accessed on the device, data integrity and stability. This prevents malicious apps from tampering with another apps files or user data and altering behaviour of another app. If this behaviour is altered at the device level i.e on jailbreak or rooted phones, the phone is exposed to a huge security risk.
This is a very volatile portion of Mobile App Development as it's consistently going through changes.

What are scenarios where you would use this?

Usually the best scenarios to implement this is when a publisher has multiple apps. These allows the publisher to use the same authentication token so the user doesn't log in twice, share the same user profile and preferences across apps. Shared analytics configuration, shared feature flags from the same remote config store.
Additionally, It allows devs to write the Local storage/Repository layer as an independent package that can be plugged into any app, ensuring consistency and prevents you from writing the same feature twice across multiple products.

Companies that use this?

Multiple companies are know to share user sessions across apps. It may not be implemented same way it is in the post. but It is known the Google's apps (Gmail, Drive, Photos) all use the same user session. Facebook, Instagram and Threads all use the same user session, Microsoft shares sign-in sessions across Office apps, Apple uses App Groups for all of the Apple apps on device. Granted these big names have existed since the dawn of the modern smart phone, so their implementation can be more optimised and targeted. Most apps with an Admin console and user console mobile app will implement similar things.

What are the security implications of doing this?.

  1. A bug/security flaw in one app can unintentionally break other apps.
  2. Has to be repeated. Exposure of sensitive data from one compromised app will definitely break other apps.
  3. Possiblity of Data corruption if write access is not controlled.
  4. There could be unintentional data sharing between production and staging flavors (this can be mitigated).
  5. Regions with strict privacy laws will require you to disclose this behaviour of your apps in your privacy policy. In General, client-side apps are never 100% safe, but you can make things harder for the attacker, and also unexpectedly make things easier for an attacker.

Implementation:

Android:

The sharedUserId is a publisher level identifier. You can find more information from this Stackoverflow discussion.

Update the Android manifest to take a sharedUserId

<manifest xmlns:android="http://schemas.android.com/apk/res/android" 
xmlns:tools="http://schemas.android.com/tools" 
package="com.example.org" 
android:sharedUserId="${sharedUserId}">
<!-- rest of the manifest code -->
Enter fullscreen mode Exit fullscreen mode

Provide the sharedUserId string for different flavors in app/build.gradle

//Previous code
android {
    productFlavors {
        staging {
            dimension "flavor-type"
            applicationId "com.example.staging"
            resValue "string", "app_name", "Example App Staging"
            manifestPlaceholders = [
                sharedUserId : "com.example.shared.staging" 
            ]

        }
        production {
            dimension "flavor-type"
            applicationId "com.example.org"
            resValue "string", "app_name", "Example App Production"
            manifestPlaceholders = [
                sharedUserId : "com.example.shared.production"
            ]
        }
    }
// Other code in android in app/build.gradle 
Enter fullscreen mode Exit fullscreen mode

Per the documentation the Android sharedUserId is a deprecated feature and apps should use proper communication mechanisms, such as services and content providers, to facilitate interoperability between shared components _(Article on this Loading...) _. But this provides a much easier implementation than writing across threads to the android platform thread.

iOS:

Using Xcode. you will have to setup App Group Entitlements. You can read more about that here.
You can set up app group entitlements by following these article by John Baker. You can ignore the Accessing Shared Data portion as we will do that on Flutter.

Flutter:

You can create a class for Initialising your local storage, and get the storage directory for this local storage when initialising the app. on iOS I use the Flutter App Group Directory package. Its a simple package that calls the underlying iOS implementation for getting storage directory for the app group. Using Hive as the Local Storage for this example. The local storage can easily be changed to any local storage. on Android, we call the getExternalStorageDirectory() as this directory is accessible to the shared application sandboxes.

class LocalStorageInit {
  static Future<void> init() async {

    if (Platform.isIOS) {
// App Group Directory on iOS.
      final sharedDirectory = await FlutterAppGroupDirectory.getAppGroupDirectory('group.com.example');

      Hive.init(sharedDirectory?.path);

    } else if(Platform.isAndroid {
      final sharedDir = await getExternalStorageDirectory();
      if (!(sharedDir?.existsSync() ?? false)) {
      //Creates the shared directory if it doesn't exist.
        sharedDir?.createSync(recursive: true);
      }

      Hive.init(sharedDir?.path);
    }
    _registerAdapters();
  }

  /// Register Hive Adapters for each model here.
  static void _registerAdapters() {
    Hive.registerAdapter(CustomHiveAdapters());
  }
}

Enter fullscreen mode Exit fullscreen mode

Issues:

I haven't noticed any issues on Android, but on iOS if you have boxes persistently opened, say streaming data from a Hive Box, there's an application has crashed warning when app is moved to background. I welcome contributions that can help me resolve this. To counter this I close boxes whenever the app is in background.
Using this Widget.

class AppLifeCycleWidget extends StatefulWidget {
  const AppLifeCycleWidget({
    required this.child,
    super.key,
  });

  final Widget child;

  @override
  State<AppLifeCycleWidget> createState() => _AppLifeCycleWidgetState();
}

class _AppLifeCycleWidgetState extends State<AppLifeCycleWidget> with WidgetsBindingObserver {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.paused) {
      closeHiveBoxes();
    }
  }

Future<void> closeHiveBoxes() async {
  final boxes = <Box<dynamic>>[
    Hive.box<User>('user')
  ];

  await Future.wait(
    boxes.map((box) => box.close()),
  );
}

  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
}
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls & How to Avoid Them

  1. Things That Can Go Wrong with Shared Storage
  2. Forgetting to set the same signing key on Android.
  3. Using different App Group IDs on iOS builds.
  4. Data not syncing due to permission misconfigurations.
  5. Corrupted JSON or missing migrations.

What's next.

  1. Test Data Consistency across Apps. Confirm that both apps with these setups can Read and write shared data correctly. Handle updates without conflicts. Test using foreground, background and freshly launched.
  2. Handle Data Versioning and convert LocalStorage/Repository to a Package: When apps evolve separately, the structure of shared data can change. If one app writes a newer format and other apps can't handle it, crashes and invalid states can occur. Converting the local storage into a package helps mitigate this.
  3. Ensure the shared data is encrypted. You have to enforce it so that if your storage url is breached by 3rd party apps that have figured out your configuration, the data is not immediately accessible.
  4. Optimize the User flow Now that the apps share storage, you can create seamless user experiences, such as auto login, unified settings, shared onboarding state.
  5. Monitor Usage and Errors. Integrate analytics into your Local Storage/Repository package to track how the storage behaves in real use. track read/write failures and watch for corrupted data to spot issues early on.
  6. Document the Shared Storage Contract. Document: What keys or data structures are shared. Which app owns which piece of data. Rules for writing, updating, and deleting shared content. This is critical if multiple teams or developers work on the apps. Treat it like an API contract "clear and versioned".

I'll like to get responses; is there a better way to achieve this?. Do you have any recommendations for improving or modifying?. Let me hear your thoughts.

Thank you!.

Top comments (0)