$30 off During Our Annual Pro Sale. View Details »

From Java to Kotlin Multiplatform

Jossi Wolf
December 11, 2019

From Java to Kotlin Multiplatform

Given at GDG Munich Android, Kotlin Usergroup Munich and Flutter Meetup Munich's joint special event 12/2019.

Jossi Wolf

December 11, 2019
Tweet

More Decks by Jossi Wolf

Other Decks in Programming

Transcript

  1. Jossi Wolf
    @jossiwolf
    From Java
    To Kotlin
    Multiplatform

    View Slide

  2. @jossiwolf
    # Kotlin Multiplatform

    View Slide

  3. @jossiwolf
    Android
    Web
    Desktop
    iOS

    View Slide

  4. @jossiwolf
    data class RabbitEntity(
    val name: String,
    val colour: Colour
    )

    View Slide

  5. @jossiwolf
    internal fun calcRangeTicks() {
    var dx = (actual_maxx - actual_minx).toDouble()
    var dy = (actual_maxy - actual_miny).toDouble()
    val sw = getWidth()
    val sh = getHeight()
    val border = 1.09345
    if (Math.abs(last_minx - actual_minx) + Math.abs(last_maxx - actual_maxx) > 0.1 * (actual_maxx - actual_minx)) {
    mTickX = calcTick(sw, dx).toFloat()
    dx = mTickX * Math.ceil(border * dx / mTickX)
    var tx = (actual_minx + actual_maxx - dx) / 2
    tx = mTickX * Math.floor(tx / mTickX)
    minx = tx.toFloat()
    tx = (actual_minx.toDouble() + actual_maxx.toDouble() + dx) / 2
    tx = mTickX * Math.ceil(tx / mTickX)
    maxx = tx.toFloat()
    last_minx = actual_minx
    last_maxx = actual_maxx
    }
    if (Math.abs(last_miny - actual_miny) + Math.abs(last_maxy - actual_maxy) > 0.1 * (actual_maxy - actual_miny)) {
    mTickY = calcTick(sh, dy).toFloat()
    dy = mTickY * Math.ceil(border * dy / mTickY)
    var ty = (actual_miny + actual_maxy - dy) / 2
    ty = mTickY * Math.floor(ty / mTickY)
    miny = ty.toFloat()
    ty = (actual_miny.toDouble() + actual_maxy.toDouble() + dy) / 2
    ty = mTickY * Math.ceil(ty / mTickY)
    maxy = ty.toFloat()
    last_miny = actual_miny
    last_maxy = actual_maxy
    }
    }

    View Slide

  6. @jossiwolf
    Android
    Web
    Desktop
    iOS

    View Slide

  7. @jossiwolf
    Android
    Web
    Desktop
    iOS
    Common

    View Slide

  8. @jossiwolf
    Common

    View Slide

  9. @jossiwolf
    Common

    View Slide

  10. @jossiwolf
    Common
    (e.g. Data Models)

    View Slide

  11. @jossiwolf
    //Common
    expect val platform: String

    View Slide

  12. @jossiwolf
    //JVM
    actual val platform = "JVM"
    //JS
    actual val platform = "JS"

    View Slide

  13. @jossiwolf
    expect val platform: String
    fun main() {
    println("Running on $platform")
    }
    //JVM
    actual val platform = "JVM"
    //JS
    actual val platform = "JS"

    View Slide

  14. @jossiwolf
    $ gradle compileJS
    $ "Running on JS"
    $ gradle compileJVM
    $ "Running on JVM"

    View Slide

  15. @jossiwolf
    # Converting

    View Slide

  16. @jossiwolf
    1.Identify your
    components

    View Slide

  17. @jossiwolf

    View Slide

  18. @jossiwolf
    UI

    View Slide

  19. @jossiwolf
    AnimationPanel CycleView
    MainPanel
    CycleEdit
    Directly UI-related

    View Slide

  20. @jossiwolf
    class MainPanel extends JFrame

    View Slide

  21. @jossiwolf
    class AnimationPanel extends JPanel

    View Slide

  22. @jossiwolf
    class CycleEdit extends JPanel

    View Slide

  23. @jossiwolf
    class CycleEdit extends JPanel
    Class Not Found!
    //Common Module

    View Slide

  24. @jossiwolf
    expect interface JPanel
    class CycleEdit extends JPanel
    //Common Module

    View Slide

  25. @jossiwolf
    expect interface JPanel
    class CycleEdit extends JPanel
    //Common Module
    No actual declaration in JVM/JS found.

    View Slide

  26. @jossiwolf
    actual typealias JPanel = swing.JPanel
    //JVM

    View Slide

  27. @jossiwolf
    expect interface JPanel
    class CycleEdit extends JPanel
    //Common Module
    No actual declaration in JS found.

    View Slide

  28. @jossiwolf
    //JS
    actual class JPanel {
    }

    View Slide

  29. @jossiwolf
    //JS
    actual class JFrame(val title: String? = null) {
    fun draw() = ...
    }

    View Slide

  30. @jossiwolf
    Separating your concerns
    is important!

    View Slide

  31. @jossiwolf
    Your UI should not be
    shared.

    View Slide

  32. @jossiwolf

    View Slide

  33. @jossiwolf
    2. Convert your models

    View Slide

  34. @jossiwolf
    Models
    CycleModel CycleSetModel
    HyperSpline
    Easing
    Interpolator MonotoneSpline
    LinearInterpolator Oscillator

    View Slide

  35. @jossiwolf
    Models
    CycleModel CycleSetModel
    HyperSpline
    Easing
    Interpolator MonotoneSpline
    LinearInterpolator Oscillator
    Data
    Domain

    View Slide

  36. @jossiwolf
    2.1 Convert your data
    models

    View Slide

  37. @jossiwolf
    abstract class Interpolator {
    static final int SPLINE = 0;
    static final int LINEAR = 1;
    abstract void getPos(double t, double[] v);
    abstract void getPos(double t, float[] v);
    abstract double getPos(double t, int j);
    abstract void getSlope(double t, double[] v);
    abstract double getSlope(double t, int j);
    abstract double[] getTimePoints();
    }

    View Slide

  38. @jossiwolf
    abstract class Interpolator {
    abstract val timePoints: DoubleArray
    abstract fun getPos(t: Double, v: DoubleArray)
    abstract fun getPos(t: Double, v: FloatArray)
    abstract fun getPos(t: Double, j: Int): Double
    abstract fun getSlope(t: Double, v: DoubleArray)
    abstract fun getSlope(t: Double, j: Int): Double
    }

    View Slide

  39. @jossiwolf
    Your data models are
    probably the easiest to
    start with!

    View Slide

  40. @jossiwolf
    …but what about logic?

    View Slide

  41. @jossiwolf
    private fun getP(time: Double): Double {
    var index = mPosition.binarySearch(time)
    var p = 0.0
    if (index > 0) {
    p = 1.0
    } else if (index != 0) {
    index = -index - 1
    val m =
    (mPeriod[index] - mPeriod[index - 1])
    / (mPosition[index] - mPosition[index - 1])
    p = (mArea[index - 1]
    + (mPeriod[index - 1] - m * mPosition[index - 1])
    * (time - mPosition[index - 1])
    + m * (time * time - mPosition[index - 1]
    * mPosition[index - 1]) / 2)
    }
    return p
    }

    View Slide

  42. @jossiwolf
    private fun getP(time: Double): Double {
    val index = mPosition.binarySearch(time)
    return when (index > 0) {
    true -> 1.0
    false -> {
    val searchIndex = -index - 1
    val acceleration = calculateAcceleration(searchIndex)
    return getArea(acceleration)
    }
    }
    }

    View Slide

  43. @jossiwolf
    If you have messy code, now
    is the time to clean it up!

    View Slide

  44. @jossiwolf
    * Make use of the
    Collections API

    View Slide

  45. @jossiwolf
    * Make sure you don‘t
    depend on Java Math APIs!

    View Slide

  46. @jossiwolf
    Models
    CycleModel CycleSetModel
    HyperSpline
    Easing
    Interpolator MonotoneSpline
    LinearInterpolator Oscillator
    Data
    Domain

    View Slide

  47. @jossiwolf
    2.2 Convert your domain
    models

    View Slide

  48. @jossiwolf
    CycleModel.java
    614 LOC :O

    View Slide

  49. @jossiwolf
    public class MainPanel extends JFrame {
    JTextArea myXmlOutput = new JTextArea();
    JScrollPane myXmlScrollPane = new JScrollPane(myXmlOutput);
    JButton myPlayButton = new JButton("Play");
    JComboBox baseMovement = new JComboBox<>(AnimationPanel.MOVE_NAMES);
    JComboBox duration = new JComboBox<>(AnimationPanel.DURATION);
    JComboBox easing = new JComboBox<>(AnimationPanel.EASING_OPTIONS);
    JPanel main = new JPanel(new BorderLayout(5,5));
    JPanel main1 = new JPanel(new BorderLayout(5,5));
    JPanel main2 = new JPanel(new BorderLayout(5,5));
    GridLayout myGraphLayout= new GridLayout(1, 1);
    JPanel myGraphs = new JPanel(myGraphLayout);
    JMenuBar topMenu= new JMenuBar();
    JPanel base = new JPanel(new BorderLayout());
    JTabbedPane myCycleEditTabs = new JTabbedPane();
    CycleSetModel myCycleSetModel = new CycleSetModel(myXmlOutput);
    private CycleSetModel.Cycle createCycle() {
    return myCycleSetModel.createCycle();
    }
    AnimationPanel animationPanel = new AnimationPanel(myCycleSetModel, myPlayButton);
    public static JButtoncreateTabbButton(String text) {
    JButton ret = new JButton(text);
    ret.setBorder(null);
    if (text == null) {
    ret.setIcon(UIManager.getIcon("InternalFrame.paletteCloseIcon"));
    }
    ret.setFocusPainted(false);
    ret.setContentAreaFilled(false);
    ret.setBorderPainted(true);
    ret.setBackground(null);
    ret.setHorizontalTextPosition(SwingConstants.LEFT);
    ret.setMargin(new Insets(0, 0, 0, 0));
    return ret;
    }
    MainPanel() {
    super("Cycle Editor");
    setBounds(new Rectangle(1000, 900));
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    myCycleSetModel.myMainPanel = this;
    CycleSetModel.CyclemyCycle;
    CycleEdit cycleEdit;
    myCycle = createCycle();
    myGraphs.add(myCycle.myView);
    main.setBorder(new EmptyBorder(new Insets(5, 5, 5, 5)));
    main.add(main1,BorderLayout.CENTER);
    main.add(main2,BorderLayout.SOUTH);
    main2.add(animationPanel,BorderLayout.CENTER);
    main1.add(myGraphs,BorderLayout.CENTER);
    main2.add(myXmlScrollPane,BorderLayout.EAST);
    main1.add(base,BorderLayout.EAST);
    myXmlScrollPane.setPreferredSize(new Dimension(100, 300));
    BasicArrowButtonnext = new BasicArrowButton(BasicArrowButton.EAST);
    BasicArrowButtonprev = new BasicArrowButton(BasicArrowButton.WEST);
    myCycle.myModel.delete= next;
    // add the first panel
    cycleEdit = new CycleEdit(myCycle.myView, myCycle.myModel, animationPanel);
    myCycle.myControl = cycleEdit;
    JScrollPane scrollPane = new JScrollPane(cycleEdit);
    myCycleEditTabs.add(scrollPane);
    cycleEdit.updateTabName(myCycle.myModel.getAttName());
    // add the add more panel
    myCycleEditTabs.add(new JPanel(), "+");
    JButton newTabbButton = createTabbButton("+");
    newTabbButton.addActionListener(e -> createNewCycle());
    myCycleEditTabs.setTabComponentAt(1, newTabbButton);
    base.add(myCycleEditTabs);
    JPanel bottomControls = new JPanel();
    base.add(bottomControls, BorderLayout.SOUTH);
    myPlayButton.setText("XXXXX");
    myPlayButton.setPreferredSize(myPlayButton.getPreferredSize());
    myPlayButton.setText("Play");
    bottomControls.add(myPlayButton);
    baseMovement
    .addActionListener((e) -> animationPanel.setMovement(baseMovement.getSelectedIndex()));
    bottomControls.add(baseMovement);
    duration.setSelectedIndex(AnimationPanel.DURATION.length - 1);
    duration.addActionListener((e) -> animationPanel.setDurationIndex(duration.getSelectedIndex()));
    bottomControls.add(duration);
    easing.addActionListener((e) -> animationPanel.setEasing((String) easing.getSelectedItem()));
    bottomControls.add(easing);
    myXmlScrollPane.setPreferredSize(base.getPreferredSize());
    setContentPane(main);
    validate();
    myCycle.myModel.update();
    JMenu menu = new JMenu("File");
    topMenu.add(menu);
    JMenuItem menuItem = new JMenuItem("parse xml", KeyEvent.VK_T);
    menuItem.addActionListener(e -> myCycle.myModelSet.parse());
    menu.add(menuItem);
    menu = new JMenu("Examples");
    topMenu.add(menu);
    for (int i = 0; i < KeyCycleExamples.all.length; i++) {
    String text = KeyCycleExamples.all[i][1];
    int speed = Integer.parseInt(KeyCycleExamples.all[i][2]);
    int movement = Integer.parseInt(KeyCycleExamples.all[i][3]);
    int easingType = Integer.parseInt(KeyCycleExamples.all[i][4]);
    menuItem = new JMenuItem(KeyCycleExamples.all[i][0]);
    menuItem.addActionListener(e -> {
    myXmlOutput.setText(text);
    duration.setSelectedIndex(speed);
    baseMovement.setSelectedIndex(movement);
    easing.setSelectedIndex(easingType);
    myCycle.myModelSet.parse();
    animationPanel.play();
    });
    menu.add(menuItem);
    }
    menu = new JMenu("Cycle");
    menuItem = new JMenuItem("Add cycle", KeyEvent.VK_A);
    menuItem.addActionListener(e -> createNewCycle());
    menu.add(menuItem);
    menuItem = new JMenuItem("Remove cycle", KeyEvent.VK_R);
    menuItem.addActionListener(e -> removeCurrentCycle());
    menu.add(menuItem);
    topMenu.add(menu);
    menuItem = new JMenuItem("Play", KeyEvent.VK_P);
    menuItem.addActionListener(e -> animationPanel.play());
    topMenu.add(menuItem);
    this.setJMenuBar(topMenu);
    }
    public CycleSetModel.CyclecreateNewCycle() {
    CycleSetModel.Cyclecycle = createCycle();
    CycleEdit cycleEdit = new CycleEdit(cycle.myView, cycle.myModel, animationPanel);
    cycle.myControl = cycleEdit;
    cycleEdit.setRemoveCallback(e -> removeCycle(cycle));
    int count = myCycleEditTabs.getTabCount();
    myCycleEditTabs.insertTab("label", null, cycleEdit, "tooltip", count- 1);
    cycleEdit.updateTabName(cycle.myModel.getAttName());
    myGraphs.add(cycle.myView);
    myGraphLayout.setRows(myCycleEditTabs.getTabCount() - 1);
    myGraphs.validate();
    return cycle;
    }
    public static int getTabbIndex(JComponentcomponent) {
    Container tabb = component.getParent();
    Component lastComponent = component;
    while (!(tabb instanceof JTabbedPane)) {
    lastComponent = tabb;
    tabb = tabb.getParent();
    }
    return ((JTabbedPane) tabb).indexOfComponent(lastComponent);
    }
    void removeCurrentCycle() {
    int i = myCycleEditTabs.getSelectedIndex();
    removeCycle(myCycleSetModel.myCycles.get(i));
    }
    void removeCycle(CycleSetModel.Cycle cycle) {
    if (myCycleEditTabs.getTabCount() < 3) { // cant remove the last one
    return;
    }
    if (cycle.myControl != null) {
    myCycleEditTabs.remove(getTabbIndex(cycle.myControl));
    }
    myGraphs.remove(cycle.myView);
    myGraphs.validate();
    myGraphLayout.setRows(myCycleEditTabs.getTabCount() - 1);
    myCycleSetModel.removeCycle(cycle);
    myCycleEditTabs.setSelectedIndex(0);
    }
    public static void main(String[] arg) {
    MainPanel f = new MainPanel();
    f.setVisible(true);
    }
    }

    View Slide

  50. @jossiwolf
    class CycleModel {
    public void addActionListener(ActionListener listener)
    public String[] getStrings()
    public void delete()
    public void add()
    public void setCycle(CycleView cycleView)
    public void update()
    public float getComputedValue(float v)
    public void setDot(float x, float y)
    public void setPos()
    public void setPeriod()
    public void setAmp()
    public void setOffset()
    void setMode()
    public void setSelected(int selectedIndex)
    public void setUIElements(JSlider pos, JSlider period, JSlider amp, JSlider off,
    JComboBox mode)
    void updateUIelements()
    public void changeSelection(int delta)
    class ParseResults {
    void add()
    }
    public static String trimDp(String v)
    public void parseXML(String str)
    public String getKeyFrames()
    public void generateXML()
    String getAttName()
    public void setAttr(int selectedIndex)
    public void selectClosest(Point2D p)
    public void setTarget(JTextField target)
    }

    View Slide

  51. @jossiwolf
    class CycleModel {
    public void addActionListener(ActionListener listener)
    public String[] getStrings()
    public void delete()
    public void add()
    public void setCycle(CycleView cycleView)
    public void update()
    public float getComputedValue(float v)
    public void setDot(float x, float y)
    public void setPos()
    public void setPeriod()
    public void setAmp()
    public void setOffset()
    void setMode()
    public void setSelected(int selectedIndex)
    public void setUIElements(…)
    void updateUIelements()
    public void changeSelection(int delta)
    class ParseResults {
    void add()
    }
    public static String trimDp(String v)
    public void parseXML(String str)
    public String getKeyFrames()
    public void generateXML()
    String getAttName()
    public void setAttr(int selectedIndex)
    public void selectClosest(Point2D p)
    public void setTarget(JTextField target)
    }

    View Slide

  52. @jossiwolf
    class CycleModel {
    final int POS = 0;
    final int PERIOD = 1;
    final int AMP = 2;
    final int OFFSET = 3;
    public int selected = 3;
    ActionListener listener;
    CycleView mCycleView;
    CycleSetModel.Cycle myCycle;
    public JTextField mKeyCycleNo;
    private JComboBox mMode;
    JButton delete, add;
    public JSlider mPos, mPeriod, mAmp, mOff;
    ...
    }

    View Slide

  53. @jossiwolf
    class CycleSetModel {
    MainPanel myMainPanel;
    JTextArea myXmlOutput;
    static class Cycle {
    CycleSetModel myModelSet;
    CycleModel myModel;
    CycleView myView;
    CycleEdit myControl;
    Cycle(CycleSetModel set) {
    myModelSet = set;
    myView = new CycleView(this);
    myModel = new CycleModel(this);
    }
    }
    CycleSetModel(JTextArea xmlOutput) {
    myXmlOutput = xmlOutput;
    }

    View Slide

  54. @jossiwolf
    Separating your concerns
    is important!

    View Slide

  55. @jossiwolf
    If you have messy code, now
    is the time to clean it up!

    View Slide

  56. @jossiwolf
    # Refactoring really
    really messy code

    View Slide

  57. @jossiwolf https://i.imgur.com/AdiBPrO.jpg

    View Slide

  58. @jossiwolf
    Model
    UI

    View Slide

  59. @jossiwolf
    Model
    UI
    State
    (User)
    Actions

    View Slide

  60. @jossiwolf
    Model

    View Slide

  61. @jossiwolf
    Model
    Repository

    View Slide

  62. @jossiwolf
    interface RabbitPicturesAPI {
    @GET("randomPicture")
    fun getRabbitPicture(): RabbitPicture
    }

    View Slide

  63. @jossiwolf
    class RabbitPicturesRepository: RabbitPicturesAPI {
    override fun getRabbitPicture() =
    document.xhr.request(...)
    }

    View Slide

  64. @jossiwolf
    Model
    Repository

    View Slide

  65. @jossiwolf
    Model
    Repository
    Presentation

    View Slide

  66. @jossiwolf
    class CycleModel {
    public void addActionListener(ActionListener listener)
    public String[] getStrings()
    public void delete()
    public void add()
    public void setCycle(CycleView cycleView)
    public void update()
    public float getComputedValue(float v)
    public void setDot(float x, float y)
    public void setPos()
    public void setPeriod()
    public void setAmp()
    public void setOffset()
    void setMode()
    public void setSelected(int selectedIndex)
    public void setUIElements(JSlider pos, JSlider period, JSlider amp, JSlider off,
    JComboBox mode)
    void updateUIelements()
    public void changeSelection(int delta)
    class ParseResults {
    void add()
    }
    public static String trimDp(String v)
    public void parseXML(String str)
    public String getKeyFrames()
    public void generateXML()
    String getAttName()
    public void setAttr(int selectedIndex)
    public void selectClosest(Point2D p)
    public void setTarget(JTextField target)
    }

    View Slide

  67. @jossiwolf
    Extract instead of
    removing.

    View Slide

  68. @jossiwolf
    public void parseXML(String str) {
    try {
    InputStream inputStream = new
    ByteArrayInputStream(str.getBytes(Charset.forName("UTF-8")));
    SAXParserFactory factory = SAXParserFactory.newInstance();
    SAXParser saxParser = factory.newSAXParser();
    ParseResults results = new ParseResults();
    saxParser.parse(inputStream, new DefaultHandler(){ ... });
    values = results.values;
    selected = values[POS].length / 2;
    mMode.setSelectedIndex(results.shape);
    mTarget.setText(results.target);
    mAttrIndex = results.valueType.ordinal();
    update();
    } catch (ParserConfigurationException e) { ... }
    }

    View Slide

  69. @jossiwolf
    class CycleModel(val cycle: Cycle) {
    fun getKeyFrames(): List = ...
    fun selectClosest(toPoint: Point2D) = ...
    }

    View Slide

  70. @jossiwolf
    # Don‘t convert UI & Repo
    # Free your Presentation
    layer of UI
    dependencies

    View Slide

  71. @jossiwolf
    fun updateUIelements() {
    inCallBack = true
    val max = calculateMax()
    val min = calculateMin()
    val middle = max / 2
    mPos.setEnabled(middle)
    delete!!.setEnabled(middle)
    }

    View Slide

  72. @jossiwolf
    # Decoupling your
    Presentation Layer

    View Slide

  73. @jossiwolf
    fun updateUIelements() {
    inCallBack = true
    val max = calculateMax()
    val min = calculateMin()
    val middle = max / 2
    mPos.setEnabled(middle)
    delete!!.setEnabled(middle)
    }

    View Slide

  74. @jossiwolf
    data class CycleViewState(
    val isMedian: Boolean,
    val positionMin: Int,
    val positionMax: Int
    )

    View Slide

  75. @jossiwolf
    suspend fun updateUIElements() {
    val min = calculateMin()
    val max = calculateMax()
    val isMiddle = ...
    val state = CycleViewState(isMiddle, min, max)
    stateFlow.emit(state)
    }

    View Slide

  76. @jossiwolf
    class CycleModel {
    // Couroutines are available in common modules
    val stateFlow = flow { … }
    }

    View Slide

  77. @jossiwolf
    class CycleActivity: AppCompatActivity() {
    }

    View Slide

  78. @jossiwolf
    class CycleActivity: AppCompatActivity() {
    private val cycleModel by lazy { CycleModel() }
    override fun onCreate() {
    cycleModel.stateFlow.collect(::render)
    }
    private suspend fun render(state: CycleViewState) {

    }
    }

    View Slide

  79. @jossiwolf
    # Introduce a View State
    # Make your data flow
    unidirectional
    # Don‘t have hard coupling
    between layers

    View Slide

  80. @jossiwolf
    Let each platform do what
    it does best.

    View Slide

  81. Jossi Wolf
    @jossiwolf
    From Java
    To Kotlin
    Multiplatform

    View Slide