mercredi 3 février 2021

A developer journey with Flutter

Allez sur www.menti.com et utilisez le code 41 48 08 9

Peut-on convaincre un développeur mobile natif de faire du Flutter ?

OUI

Peut-on convaincre un développeur mobile natif de faire du Flutter ?

Peut-on convaincre un développeur mobile natif de faire du Flutter sans recourir à l'argent ou aux menaces?

pour du une app iOS/Android

Natif

Hybrique

prix

temps

compétences

performances

UI

animations

taille de l'app

x2

Peut-on convaincre un développeur mobile natif de faire du Flutter sans recourir à l'argent ou aux menaces?

Peut-on convaincre un développeur mobile natif de faire une application Android au design atypique utilisant une vue native openGL et publiée sous plusieurs variantes en du Flutter sans recourir à l'argent ou aux menaces?

Le dev

Florian Paillard

http://whois.sertook.com/

# ~ curl http://whois.sertook.com/florian-paillard
{
    name: "Florian Paillard",
    company: "Eyeo",
    function: "Mobile developer",
    birthday: "676133927",
    projects: {
      eyeo: {
        adblock-browser: "ads free chromium browser",
        crumbs: "Privacy tools"
      },
      lm: {
      	pandalab : "devicelab software",
        pandroid : "open source mobile archetype",
        delivery : "open source delivery plugin",
        colibri-apps: "store apps",
        innovation: "VR, wear os, chatbot, prototyping ..."
      },
      sertook: {
      	robolytics : "mobile analytics tag manager",
        one-jitsu : "Jiu-jitsu practitioners network",
        booth-app : "FatBooth, AgingBooth, ..."
      }
    },
    links : {
      twitter: "@paillard_f",
      linkedin : "http://whois.sertook.com",
      robolytics : "https://robolytics.sertook.com"
    }
}

Le client

Sketches

2014 - 2016

Java

6k utilisateurs

3.6/5.0

2012 - 2021

Objectif - C

1M utilisateurs actifs

4.4/5.0

Pourquoi flutter ?

Client

  • Peu convaincu par le dev natif
  • Publier pour Linux et Windows
  • Backup iOS

Moi

  • Monter en compétences
  • Tester les limites de Flutter
  • Rendre fier Mehdi
  • Pouvoir faire des talks Flutter

Apprendre Flutter

Dart

  • Syntaxe connue
  • Dart Package Manager et build system
  • Runtime (DartVM/Native/JavaScript)
  • Hot reloading
  • Generation de code

Dart

Named and optional parameters

class Person {
  String firstName;
  String lastName;
  int age;
  String occupation;

  Person({this.firstName, this.lastName, this.age, this.occupation = 'unknown'});
}

final joe = Person(
  firstName: 'Joe',
  lastName: 'Doe',
  occupation: 'plumber',
  age: 32,
);

Dart

Cascade notation

final element = Element.div();
final style = element.style;
style.width = '50%';
style.height = '4em';
style.padding = '1em';
append(element);

append(Element.div()
  ..style.width = '50%'
  ..style.height = '4em'
  ..style.padding = '1em');

Dart

Null Safety

print(Person().lastName?.toLowerCase()?.indexOf('a')); // null

print(Person().lastName?.toLowerCase()?.indexOf('a') ?? -1); // -1
String s;

void something(String newValue) {
  // if s is null, it gets a new value!
  s ??= newValue;
}

Dart

Collection if/for and spreads

print(['run', if (Platform.isWindows) '.exe']);

final numbers = Iterable.generate(10, (index) => index + 1); // 1 to 10

final evenNumbers = [0, for (var n in numbers) if (n % 2 == 0) n];

print(evenNumbers); // [0, 2, 4, 6, 8, 10]


List<int> withBothEnds(int endElement, List<int> list) =>
  [endElement, ...list, endElement];

// prints "Both ends: [42, 1, 2, 3, 42]"
print("Both ends: ${withBothEnds(42, [1, 2, 3])}");

Dart

Asynchronous Programming

Future main() async {
  var server = await HttpServer.bind(
    InternetAddress.loopbackIPv4,
    4040,
  );
  print('Listening on localhost:${server.port}');

  await for (HttpRequest request in server) {
    request.response.write('Hello, world!');
    await request.response.close();
  }
}

Dart

Extension

extension NumberParsing on String {
  int parseInt() {
    return int.parse(this);
  }
  // ···
}

print('42'.parseInt()); // Use an extension method.

https://flutter.dev/

https://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw

Resources

Plus simple

  • Cycle de vie
  • Activity/Fragment/ViewController
  • UIThread
  • Adapter/Holder
  • Layout

UI as code

  • React
  • Swift UI
  • Jetpack compose

Plugin natif

FlutterPlugin

class MainActivity : FlutterFragmentActivity() {
    
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        flutterEngine.plugins.add(InfosPlugin())
    }
    
}
class InfosPlugin : MethodCallHandler, FlutterPlugin, ActivityAware {

    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        val messenger = binding.binaryMessenger
        val channel = MethodChannel(messenger, "com.tayasui.sketches/infos")
        channel.setMethodCallHandler(this)
    }

    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
    	when (call.method) {
             "getFlavorName" -> {
            	result.success(BuildConfig.FLAVOR)
             }
        }
    }
}

FlutterPlugin

var infosChannel = const MethodChannel('com.tayasui.sketches/infos');

var flavor = await infosChannel.invokeMethod("getFlavorName");


//infosChannel.setMethodCallHandler((MethodCall call) {});

Encore plus facile avec

Vue native

class DrawingViewPlugin : FlutterPlugin {

    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        binding.platformViewRegistry.registerViewFactory(
                "com.tayasui.sketches/drawingview", 
                DrawingViewFactory(binding.binaryMessenger)
        )
    }

    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
    }

}
class MainActivity : FlutterFragmentActivity() {
    
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        flutterEngine.plugins.add(InfosPlugin())
        flutterEngine.plugins.add(DrawingViewPlugin())
    }
    
}

Vue native

class FlutterDrawingView(context: Context, messenger: BinaryMessenger, id: Int) : 
    PlatformView, MethodChannel.MethodCallHandler {

    private val methodChannel: MethodChannel = 
                MethodChannel(messenger, "com.tayasui.sketches/drawingview_$id")
    private val view: DrawingView by lazy { DrawingView(context, this) }

    override fun getView(): View {
        return view
    }

    override fun dispose() {
        view.dispose()
    }
    
}
class DrawingViewFactory(private val messenger: BinaryMessenger) : PlatformViewFactory(StandardMessageCodec.INSTANCE) {

    override fun create(context: Context, id: Int, o: Any?): PlatformView {
        return FlutterDrawingView(context, messenger, id)
    }

}

Vue native

  @override
  Widget build(BuildContext context) {
    return AndroidView(
             viewType: 'com.tayasui.sketches/drawingview',
             onPlatformViewCreated: this._onPlatformViewCreated,
             hitTestBehavior: PlatformViewHitTestBehavior.transparent,
           );
  }


  void _onPlatformViewCreated(int id) {
    var viewChannel = new MethodChannel('com.tayasui.sketches/drawingview_$id')
  }
class DrawingView(context: Context) : GLSurfaceView(context){}


class GLSurfaceView(context: Context){

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (mDetached && (mRenderer != null)) {
            int renderMode = RENDERMODE_CONTINUOUSLY;
            if (mGLThread != null) {
                renderMode = mGLThread.getRenderMode();
            }
            mGLThread = new GLThread(mThisWeakRef);
            if (renderMode != RENDERMODE_CONTINUOUSLY) {
                mGLThread.setRenderMode(renderMode);
            }
            mGLThread.start();
        }
        mDetached = false;
    }

    @Override
    protected void onDetachedFromWindow() {
        if (mGLThread != null) {
            mGLThread.requestExitAndWait();
        }
        mDetached = true;
        super.onDetachedFromWindow();
    }
}

UI

Comme sur iOS...

ScrollConfiguration(
	behavior: SketchesBehavior(),
	child: child,
)


class SketchesBehavior extends ScrollBehavior {
  @override
  ScrollPhysics getScrollPhysics(BuildContext context) =>
      BouncingScrollPhysics();
}

Comme sur iOS...

Comme sur iOS...

Comme sur iOS...

AlertDialog(
  title: Text(this.title),,
  actions: <Widget>[
    FlatButton(
      child: Text(this.negative ?? S.of(context).s_cancel),
      onPressed: this.onNegative ?? () => Navigator.of(context).pop(false),
    ),
    FlatButton(
      child: Text(this.positive ?? S.of(context).s_ok),
      onPressed: this.onPositive ?? () => Navigator.of(context).pop(true),
    ),
  ],
);
CupertinoAlertDialog(
  title: Text(this.title),
  actions: <Widget>[
    CupertinoDialogAction(
      child: Text(this.negative ?? S.of(context).s_cancel),
      onPressed: this.onNegative ?? () => Navigator.of(context).pop(false),
    ),
    CupertinoDialogAction(
      isDefaultAction: true,
      child: Text(this.positive ?? S.of(context).s_ok),
      onPressed: this.onPositive ?? () => Navigator.of(context).pop(true),
    ),
  ],
);

Personnalisation

Personnalisation

CupertinoSwitch(
  activeColor: CupertinoColors.systemGreen,
  trackColor: CupertinoColors.secondarySystemFill,
)

Plan A : Utilisation du thème et des attributs

Personnalisation

const double _kTrackWidth = 51.0;
const double _kTrackHeight = 31.0;
const double _kTrackRadius = _kTrackHeight / 2.0;
const double _kTrackInnerStart = _kTrackHeight / 2.0;
const double _kSwitchWidth = 59.0;
const double _kSwitchHeight = 39.0;

class _RenderCupertinoSwitch extends RenderConstrainedBox {
  @override
  void paint(PaintingContext context, Offset offset) {
    //...
  }
}
  

Plan B : Surcharger le composant

Personnalisation

Plan C : Refaire le composant de 0

Personnalisation

Plan D : copier le composant

  • 535 lignes identiques
  • 6 lignes modifier
CupertinoSwitch 
(541)

ExpansionPanelList (726)

ImplicitlyAnimatedReorderableList 
(902)

 

ScaleGestureRecognizer (501)

 

PanGestureRecognizer

(161)

 

Temps de développement

5 semaines

POC

244 h

Publication

novembre 2019

janvier 2020

septembre 2020

 ≃ 420 heures

≃ 12 semaines

≃ 3 mois

Performance

Taille

Optimisation

Communication Flutter/Natif

1 à 2 ms

StandardMessageCodec

BinaryCodec

JsonMessageCodec

StringCodec

 

public MethodChannel(BinaryMessenger messenger, String name, MethodCodec codec){}

Outils

Libs

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  image: ^2.1.14
  logger: ^0.9.1
  flutter_colorpicker: 0.3.4
  provider: ^4.1.3
  flutter_svg: ^0.19.1
  flutter_staggered_animations: ^0.1.2
  animations: ^1.0.0+5
  path_provider: ^1.6.9
  shared_preferences: ^0.5.12
  cupertino_icons: ^1.0.0
  json_annotation: ^3.0.1
  xml: ^4.2.0
  uuid: ^2.0.4
  file_picker: ^2.1.5
  exif: 1.0.3
  esys_flutter_share: ^1.0.2
  receive_sharing_intent: ^1.4.2
  firebase_core: 0.5.0+1
  firebase_crashlytics: ^0.2.1+1
  firebase_remote_config: ^0.4.0+1
  simple_animations: ^2.2.1
  mutex: ^1.0.3
  native_device_orientation: ^0.4.3
  in_app_purchase: ^0.3.4+16
  vibration: ^1.7.1
  permission_handler: ^5.0.1+1
  ext_storage: ^1.0.3
  archive: ^2.0.5
  implicitly_animated_reorderable_list: ^0.3.2
  in_app_review: ^1.0.2
  package_info: ^0.4.3+2
  flutter_custom_tabs: ^0.6.0

provider

flutter_svg
in_app_purchase
shared_preferences
firebase_*
file_picker
json_serializable/json_annotation

 

 

Organisation de code

Rangez vos sources !

1 feature = 1 dossier

Nommage

Widget:

  • Page/Dialog
  • Section
  • Skill
  • Widget

Provider:

  • Service
  • Presenter

Bilan

Peut-on convaincre un développeur mobile natif de faire une application Android au design atypique utilisant une vue native openGL et publiée sous plusieurs variantes en du Flutter sans recourir à l'argent ou aux menaces?

6k

3.6 star

150k

4.2 star

VS

Technique

Humain

A dev journey with Flutter

By Florian Paillard

A dev journey with Flutter

  • 324