57

I am using shared_preferences in my Flutter application for iOS and Android. On the web I am using the http:dart dependency (window.localStorage) itself. Since Flutter for web was merged into the Flutter repo, I want to create a cross platform solution.

This means I need to import two seperate API's. This seems not to be very good supported in Dart yet, but this is what I did:

import 'package:some_project/stub/preference_utils_stub.dart'
    if (dart.library.html) 'dart:html'
    if (dart.library.io) 'package:shared_preferences/shared_preferences.dart';

In my preference_utils_stub.dart file, I implemented all classes/variables which need to be visible during compile time:

Window window;

class SharedPreferences {
  static Future<SharedPreferences> get getInstance async {}
  setString(String key, String value) {}
  getString(String key) {}
}

class Window {
  Map<String, String> localStorage;
}

This gets rid of all errors before compilation. Now I implemented some method which checks if the application is using the web or not:

static Future<String> getString(String key) async {
    if (kIsWeb) {
       return window.localStorage[key];
    }
    SharedPreferences preferences = await SharedPreferences.getInstance;
    return preferences.getString(key);
}

However, this gives loads of errors:

lib/utils/preference_utils.dart:13:7: Error: Getter not found:
'window'.
      window.localStorage[key] = value;
      ^^^^^^ lib/utils/preference_utils.dart:15:39: Error: A value of type 'Future<SharedPreferences> Function()' can't be assigned to a
variable of type 'SharedPreferences'.
 - 'Future' is from 'dart:async'.
 - 'SharedPreferences' is from 'package:shared_preferences/shared_preferences.dart'
('../../flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.5.4+3/lib/shared_preferences.dart').
      SharedPreferences preferences = await SharedPreferences.getInstance;
                                      ^ lib/utils/preference_utils.dart:22:14: Error: Getter not found:
'window'.
      return window.localStorage[key];

And so on. How can one use different methods/classes depending on the platform without these errors? Note that I am using more dependencies this way, not just preferences. Thanks!

gi097
  • 7,313
  • 3
  • 27
  • 49
  • In my limited knowledge you should not have both the `localstorage` and `shared preferences` dependencies in the same method or class. This means the compiler cannot treeshake the either of these dependencies. Ideally the import should hide these implementations. I will try to come up with a clear implementation example. – Abhilash Chandran Nov 05 '19 at 11:51
  • You could use the global boolean kIsWeb which can tell you whether or not the app was compiled to run on the web. Documentation: https://api.flutter.dev/flutter/foundation/kIsWeb-constant.html if (kIsWeb) { // running on the web! initialize web db }else{ // use shared preferences } – Shamik Chodankar Dec 08 '19 at 19:38

3 Answers3

122

Here is my approach to your issue. This is based on the implementations from http package as in here.

The core idea is as follows.

  1. Create an abstract class to define the methods you will need to use.
  2. Create implementations specific to web and android dependencies which extends this abstract class.
  3. Create a stub which exposes a method to return the instance of this abstract implementation. This is only to keep the dart analysis tool happy.
  4. In the abstract class import this stub file along with the conditional imports specific for mobile and web. Then in its factory constructor return the instance of the specific implementation. This will be handled automatically by conditional import if written correctly.

Step-1 and 4:

import 'key_finder_stub.dart'
    // ignore: uri_does_not_exist
    if (dart.library.io) 'package:flutter_conditional_dependencies_example/storage/shared_pref_key_finder.dart'
    // ignore: uri_does_not_exist
    if (dart.library.html) 'package:flutter_conditional_dependencies_example/storage/web_key_finder.dart';

abstract class KeyFinder {

  // some generic methods to be exposed.

  /// returns a value based on the key
  String getKeyValue(String key) {
    return "I am from the interface";
  }

  /// stores a key value pair in the respective storage.
  void setKeyValue(String key, String value) {}

  /// factory constructor to return the correct implementation.
  factory KeyFinder() => getKeyFinder();
}

Step-2.1: Web Key finder

import 'dart:html';

import 'package:flutter_conditional_dependencies_example/storage/key_finder_interface.dart';

Window windowLoc;

class WebKeyFinder implements KeyFinder {

  WebKeyFinder() {
    windowLoc = window;
    print("Widnow is initialized");
    // storing something initially just to make sure it works. :)
    windowLoc.localStorage["MyKey"] = "I am from web local storage";
  }

  String getKeyValue(String key) {
    return windowLoc.localStorage[key];
  }

  void setKeyValue(String key, String value) {
    windowLoc.localStorage[key] = value;
  }  
}

KeyFinder getKeyFinder() => WebKeyFinder();

Step-2.2: Mobile Key finder

import 'package:flutter_conditional_dependencies_example/storage/key_finder_interface.dart';
import 'package:shared_preferences/shared_preferences.dart';

class SharedPrefKeyFinder implements KeyFinder {
  SharedPreferences _instance;

  SharedPrefKeyFinder() {
    SharedPreferences.getInstance().then((SharedPreferences instance) {
      _instance = instance;
      // Just initializing something so that it can be fetched.
      _instance.setString("MyKey", "I am from Shared Preference");
    });
  }

  String getKeyValue(String key) {
    return _instance?.getString(key) ??
        'shared preference is not yet initialized';
  }

  void setKeyValue(String key, String value) {
    _instance?.setString(key, value);
  }

}

KeyFinder getKeyFinder() => SharedPrefKeyFinder();

Step-3:

import 'key_finder_interface.dart';

KeyFinder getKeyFinder() => throw UnsupportedError(
    'Cannot create a keyfinder without the packages dart:html or package:shared_preferences');

Then in your main.dart use the KeyFinder abstract class as if its a generic implementation. This is somewhat like an adapter pattern.

main.dart

import 'package:flutter/material.dart';
import 'package:flutter_conditional_dependencies_example/storage/key_finder_interface.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    KeyFinder keyFinder = KeyFinder();
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SafeArea(
        child: KeyValueWidget(
          keyFinder: keyFinder,
        ),
      ),
    );
  }
}

class KeyValueWidget extends StatefulWidget {
  final KeyFinder keyFinder;

  KeyValueWidget({this.keyFinder});
  @override
  _KeyValueWidgetState createState() => _KeyValueWidgetState();
}

class _KeyValueWidgetState extends State<KeyValueWidget> {
  String key = "MyKey";
  TextEditingController _keyTextController = TextEditingController();
  TextEditingController _valueTextController = TextEditingController();
  @override
  Widget build(BuildContext context) {
    return Material(
      child: Container(
        width: 200.0,
        child: Column(
          children: <Widget>[
            Expanded(
              child: Text(
                '$key / ${widget.keyFinder.getKeyValue(key)}',
                style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold),
              ),
            ),
            Expanded(
              child: TextFormField(
                decoration: InputDecoration(
                  labelText: "Key",
                  border: OutlineInputBorder(),
                ),
                controller: _keyTextController,
              ),
            ),
            Expanded(
              child: TextFormField(
                decoration: InputDecoration(
                  labelText: "Value",
                  border: OutlineInputBorder(),
                ),
                controller: _valueTextController,
              ),
            ),
            RaisedButton(
              child: Text('Save new Key/Value Pair'),
              onPressed: () {
                widget.keyFinder.setKeyValue(
                  _keyTextController.text,
                  _valueTextController.text,
                );
                setState(() {
                  key = _keyTextController.text;
                });
              },
            )
          ],
        ),
      ),
    );
  }
}

some screen shots

Web enter image description here enter image description here

mobile enter image description here

Abhilash Chandran
  • 6,803
  • 3
  • 32
  • 50
  • 4
    Thanks for this huge effort! Well done. I was in the meantime on the same way (looking in the http package as well, which is funny :)). Thanks a lot! – gi097 Nov 05 '19 at 14:07
  • 4
    Hope this helps others as well. We all learn by solving.. :-) – Abhilash Chandran Nov 05 '19 at 14:36
  • Hi tried your code worked ! ty. I then found out about the global boolean kIsWeb which can tell you whether or not the app was compiled to run on the web. Documentation: api.flutter.dev/flutter/foundation/kIsWeb-constant.html PS- New to flutter apologies in advance if I am overlooking something implementation becomes a lot simpler if you use that – Shamik Chodankar Dec 08 '19 at 19:47
  • 3
    @ShamikChodankar You are right. This boolean flag will be helpful for certain logical decision. OP tried this option as well. But the problem is, if we use both `dart:html' and `sharedpreferences` in the same function, the compiler will generate errors because it won't know about `dart:html` when compiling against a mobile device and on contrary it won't know about `sharedpreferences` when compiling against web unless its authors handle it internally. Please do share if you have a working example utilizing this flag. I am also new to flutter:). – Abhilash Chandran Dec 09 '19 at 09:29
  • 1
    Awesome, is something like this possible for iOS? I don't want to import a library on iOS. Because that library doesn't have any iOS implementation so it crashes on iOS build. – jasxir Dec 01 '20 at 12:03
  • @jasxir have you found a solution for this – SardorbekR Dec 11 '20 at 06:41
  • I searched and found that there are no such checks available from Flutter for us to differentiate between Android and iOS. So it seems currently there's no way. – jasxir Dec 14 '20 at 04:07
  • 10
    I'm seeing that the dart compiler is not importing either (not web, nor native) so when I compile I get `method not found getKeyFinder()` any idea if this is still working on dart 2.0? @AbhilashChandran – perrohunter Feb 02 '21 at 05:05
  • What if I want inside abstract class KeyFinder abstract method e.g. 'void uploadKey(File file)'. Type File is in both dart.io and dart.html. Compiler doesn't know which File is it. – Piotr Temp Feb 06 '21 at 17:58
  • 7
    Answer to @perrohunter, keep your getKeyFinder() method outside of the class. Carefully observe the AbhilashChandran class 'WebKeyFinder' you will find, what i am suggesting. Thanks for the answer, This solved my web specific 'dart.html' package issue. – Raghu Mudem Mar 02 '21 at 22:44
  • 4
    Thank you that helped so much, for those getting the method not found error, the `getKeyFinder()` method in the implementations (`SharedPrefKeyFinder` and `WebKeyFinder`) should go outside of the implementing class. – Neco Horne Apr 17 '21 at 09:28
  • Hello, is it possible to do the same to import stateful widget based on platform. I tried hard to make a check with kIsWeb flag but it blocks the android build. however, it works fine in development mode – Ishaan Puniani Sep 05 '22 at 05:09
  • @IshaanPuniani, it seems you don't understand the whole meaning of solution written by Abhilash Chandran. No need to make two pages of stateful widget based on platform. You just need to create the abstract classes and two classes which inherit it. One class for web, another one for mobile apps. – Wege Dec 30 '22 at 16:54
  • 1
    @IshaanPuniani, maybe you need to see this: https://stackoverflow.com/questions/71254421/using-google-maps-in-flutter-for-both-mobile-and-web?noredirect=1&lq=1 – Wege Dec 30 '22 at 17:11
  • chatGPT4 was unable to answer this question. This is working for me. Long live StackOverflow! – BigMarty Mar 28 '23 at 21:58
  • It worked for `dio`, thanks! I just found this a little confusing to follow. I would suggest to write the name of each file above the code blocks. The problem is that, because the stub references the interface and the interface references the stub, I thought they had to be other files or there would be a circular reference. But, no, Dart is okay with that. – GuiRitter Aug 10 '23 at 13:20
2

Thanks @AbhilashChandran I was troubling this kind of problem with the credentials of the http client.

here is my solution copied from @AbhilashChandran without interface

get_client.dart:

import 'package:http/http.dart';

Client getClient() => throw UnsupportedError('[Platform ERROR] Network client'); 

mobile_client.dart:

import 'package:http/http.dart';

Client getClient() => Client();

web_client.dart:

import 'package:http/browser_client.dart';
import 'package:http/http.dart';

Client getClient() => BrowserClient()..withCredentials = true;

network_client.dart

import 'get_client.dart'
    if (dart.library.io) 'mobile_client.dart'
    if (dart.library.html) 'web_client.dart';

import 'dart:convert';
import 'package:http/http.dart';
import '../../constants/url_paths.dart';

class NetworkClient {
  final client = getClient();

  final headers = {
    'Content-Type': 'application/json',
  };

  Future<Response> get(String path) =>
      client.get(Uri.http(url, path), headers: headers);

  Future<Response> post(String path, dynamic parameter) =>
      client.post(Uri.http(url, path),
          headers: headers, body: json.encode(parameter));

  Future<Response> put(String path, dynamic parameter) => client.put(Uri.http(url, path), headers: headers, body: json.encode(parameter));

  Future<Response> delete(String path) =>
      client.delete(Uri.http(url, path), headers: headers);
}
bk3
  • 41
  • 3
-2

you can just use the package universal_html

flutter
  • 6,188
  • 9
  • 45
  • 78