AdobeXD / xd-to-flutter-plugin

Generate assets from XD for use in an existing Flutter project
BSD 2-Clause "Simplified" License
953 stars 97 forks source link

Abstraction layer for implementing functionalities #111

Open JakubNeukirch opened 3 years ago

JakubNeukirch commented 3 years ago

We need some abstraction layer, to easily implement business logic without any interaction with generated files.

I have already a few ideas and I can contribute but firstly I would like to show what I am thinking about. And also I want to verify my ideas with other people opinion.

How to implement your own features

So let me begin with how will UI operate with custom code. Basically we would have Features abstract class in adobe_xd plugin. Its responsibility is to invoke custom code, provide UI data and return processed data to the view.

abstract class Features<DATA> {
  final ViewDataGetter viewDataGetter;
  Map<String, dynamic> get viewData => viewDataGetter();
  OnData<DATA> _onData = (_){};

  void init(OnData<DATA> onData) {
    _onData = onData;
  }

  void setData(DATA data) {
    _onData(data);
  }

  void dispose() {}

  Features(this.viewDataGetter);
}

So what we have here is:

Each UI generates its own Features for example Login Page would have features like this:

abstract class LoginFeatures extends Features<LoginData> {
  void login();

  LoginFeatures(ViewDataGetter getter): super(getter);
}

class LoginData {
  final LoginState state;

  LoginData(this.state);
}

The method login() was generated based on Tap Callback of a button from Adobe XD. LoginData is also generated - I do not have clear view how it will look yet, but probably it will be based on parameters or some additional settings of a page. And yours implementation would look like this:

class LoginFeaturesImpl extends LoginFeatures {
  final AuthApi api;

  LoginFeaturesImpl(ViewDataGetter getter, this.api):super(getter);

  @override
  void login() async {
      try{
        setData(LoginData(LoginState.loading));
        await api.login(viewData["email"], viewData["password"]);
        setData(LoginData(LoginState.success));
      }catch(error) {
        setData(LoginData(LoginState.error));
      }
  }
}

As you can see we invoke setData() to provide UI some updates based on our features. Also as you can see, you can easily take data from view through viewData["email"]. Through viewData you can also provide BuildContext if needed for some context-based operations - also we might consider adding BuildContext to every method so it will be always context from specific place in widget tree. Something like this void login(BuildContext context).

How to Provide your own Features class

For providing and injecting implementation of Features class you can use FeaturesProvider - for now it is very simple, but we may think of something more complex like BlocProvider.

FeaturesProvider would be a part of adobe_xd plugin. It contains all implementations of Features and injects them to UI. Usage of it would look like this (in main.dart file):

MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primaryColor: XDColors.primary,
        accentColor: XDColors.accent,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: FeaturesProvider(
          child: XDLoginScreen(),
          features: {
            LoginFeatures: (getter) => LoginFeaturesImpl(getter, AuthApi())
          },
      ),
    );

Argument features is just a Map<Type, Features>. Based on Type, Features implementation is injected in UI. Additional code that will be generated in UI (in this case XDLoginScreen) looks like this:

  LoginFeatures features;
  //here will be all fields from Data class
  LoginState state;

  @override
  void initState() {
    super.initState();
    features = FeaturesProvider
        .of(context)
        .getFeatures(_getViewData);
        WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
          features.init(onData);
        });
  }

  @override
  void dispose() {
    features.dispose();
    super.dispose();
  }

  void onData(LoginData data) {
      setState(() {
        state = data.state;
      });
  }

  Map<String, dynamic> _getViewData() {
    return {
      "login": _loginController.text,
      "password": _passwordController.text
    };
  }

As you probably noticed - the generated view was changed to StatefulWidget. That allows us to properly initialize and operate on Features and update UI when onData is invoked. The thing to be thought through is UI data providing. For now I used TextEdittingController but it is too case-specific. We may think about creating some DataHolder<TYPE> or something like this, so it will be more generic. Such DataHolder would be placed in components which can modify its data.

Summary

There might be some holes in this whole idea. But I am placing it here so we could polish the idea together and come up with some great solution. Maybe someone has more simple solution?