Converting a Java Desktop App to Kotlin Multiplatform

2fff9b69a69973e14026624f8c8a9672?s=47 Jossi Wolf
September 24, 2019

Converting a Java Desktop App to Kotlin Multiplatform

Talk from Droidcon Greece 2019

2fff9b69a69973e14026624f8c8a9672?s=128

Jossi Wolf

September 24, 2019
Tweet

Transcript

  1. Jossi Wolf @jossiwolf From Java To Kotlin Multiplatform

  2. #droidconGR @jossiwolf # Kotlin Multiplatform

  3. #droidconGR @jossiwolf Android Web Desktop iOS

  4. #droidconGR @jossiwolf data class RabbitEntity( val name: String, val colour:

    Colour )
  5. #droidconGR @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 } }
  6. #droidconGR @jossiwolf Android Web Desktop iOS

  7. #droidconGR @jossiwolf Android Web Desktop iOS Common

  8. #droidconGR @jossiwolf Common

  9. #droidconGR @jossiwolf Common

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

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

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

    val platform = "JS"
  13. #droidconGR @jossiwolf expect val platform: String fun main() { println("Running

    on $platform") } //JVM actual val platform = "JVM" //JS actual val platform = "JS"
  14. #droidconGR @jossiwolf $ gradle compileJS $ "Running on JS" $

    gradle compileJVM $ "Running on JVM"
  15. #droidconGR @jossiwolf # Converting

  16. #droidconGR @jossiwolf 1.Identify your components

  17. #droidconGR @jossiwolf

  18. #droidconGR @jossiwolf UI

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

  20. #droidconGR @jossiwolf class MainPanel extends JFrame

  21. #droidconGR @jossiwolf class AnimationPanel extends JPanel

  22. #droidconGR @jossiwolf class CycleEdit extends JPanel

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

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

    Module
  25. #droidconGR @jossiwolf expect interface JPanel class CycleEdit extends JPanel //Common

    Module No actual declaration in JVM/JS found.
  26. #droidconGR @jossiwolf actual typealias JFrame = swing.JFrame //JVM

  27. #droidconGR @jossiwolf expect interface JPanel class CycleEdit extends JPanel //Common

    Module No actual declaration in JS found.
  28. #droidconGR @jossiwolf //JS actual class JFrame { }

  29. #droidconGR @jossiwolf //JS class JFrame(val title: String? = null) {

    fun draw() = ... }
  30. #droidconGR @jossiwolf Separating your concerns is important!

  31. #droidconGR @jossiwolf Your UI should not be shared.

  32. #droidconGR @jossiwolf

  33. #droidconGR @jossiwolf 2. Convert your models

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

    Oscillator
  35. #droidconGR @jossiwolf Models CycleModel CycleSetModel HyperSpline Easing Interpolator MonotoneSpline LinearInterpolator

    Oscillator Data Domain
  36. #droidconGR @jossiwolf 2.1 Convert your data models

  37. #droidconGR @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(); }
  38. #droidconGR @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 }
  39. #droidconGR @jossiwolf Your data models are probably the easiest to

    start with!
  40. #droidconGR @jossiwolf …but what about logic?

  41. #droidconGR @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 }
  42. #droidconGR @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) } } }
  43. #droidconGR @jossiwolf If you have messy code, now is the

    time to clean it up!
  44. #droidconGR @jossiwolf * Make use of the Collections API

  45. #droidconGR @jossiwolf * Make sure you don‘t depend on Java

    Math APIs!
  46. #droidconGR @jossiwolf Models CycleModel CycleSetModel HyperSpline Easing Interpolator MonotoneSpline LinearInterpolator

    Oscillator Data Domain
  47. #droidconGR @jossiwolf 2.2 Convert your domain models

  48. #droidconGR @jossiwolf CycleModel.java 614 LOC :O

  49. #droidconGR @jossiwolf public class MainPanel extends JFrame { JTextArea myXmlOutput

    = new JTextArea(); JScrollPane myXmlScrollPane = new JScrollPane(myXmlOutput); JButton myPlayButton = new JButton("Play"); JComboBox<String> baseMovement = new JComboBox<>(AnimationPanel.MOVE_NAMES); JComboBox<String> duration = new JComboBox<>(AnimationPanel.DURATION); JComboBox<String> 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); } }
  50. #droidconGR @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<String> 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) }
  51. #droidconGR @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) }
  52. #droidconGR @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<String> mMode; JButton delete, add; public JSlider mPos, mPeriod, mAmp, mOff; ... }
  53. #droidconGR @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; }
  54. #droidconGR @jossiwolf Separating your concerns is important!

  55. #droidconGR @jossiwolf If you have messy code, now is the

    time to clean it up!
  56. #droidconGR @jossiwolf # Refactoring really really messy code

  57. #droidconGR @jossiwolf Model UI

  58. #droidconGR @jossiwolf Model UI State (User) Actions

  59. #droidconGR @jossiwolf Model

  60. #droidconGR @jossiwolf Model Repository

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

  62. #droidconGR @jossiwolf class RabbitPicturesRepository: RabbitPicturesAPI { override fun getRabbitPicture() =

    document.xhr.request(...) }
  63. #droidconGR @jossiwolf Model Repository

  64. #droidconGR @jossiwolf Model Repository Presentation

  65. #droidconGR @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<String> 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) }
  66. #droidconGR @jossiwolf Extract instead of removing.

  67. #droidconGR @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) { ... } }
  68. #droidconGR @jossiwolf class CycleModel(val cycle: Cycle) { fun getKeyFrames(): List<KeyFrame>

    = ... fun selectClosest(toPoint: Point2D) = ... }
  69. #droidconGR @jossiwolf # Don‘t convert UI & Repo # Free

    your Presentation layer of UI dependencies
  70. #droidconGR @jossiwolf fun updateUIelements() { inCallBack = true val max

    = calculateMax() val min = calculateMin() val middle = max / 2 mPos.setEnabled(middle) delete!!.setEnabled(middle) }
  71. #droidconGR @jossiwolf # Decoupling your Presentation Layer

  72. #droidconGR @jossiwolf fun updateUIelements() { inCallBack = true val max

    = calculateMax() val min = calculateMin() val middle = max / 2 mPos.setEnabled(middle) delete!!.setEnabled(middle) }
  73. #droidconGR @jossiwolf data class CycleViewState( val isMedian: Boolean, val positionMin:

    Int, val positionMax: Int )
  74. #droidconGR @jossiwolf suspend fun updateUIElements() { val min = calculateMin()

    val max = calculateMax() val isMiddle = ... val state = CycleViewState(isMiddle, min, max) stateFlow.emit(state) }
  75. #droidconGR @jossiwolf class CycleModel { // Couroutines are available in

    common modules val stateFlow = flow<CycleViewState> { … } }
  76. #droidconGR @jossiwolf class CycleActivity: AppCompatActivity() { }

  77. #droidconGR @jossiwolf class CycleActivity: AppCompatActivity() { private val cycleModel by

    lazy { CycleModel() } override fun onCreate() { cycleModel.stateFlow.collect(::render) } private suspend fun render(state: CycleViewState) { … } }
  78. #droidconGR @jossiwolf # Introduce a View State # Make your

    data flow unidirectional # Don‘t have hard coupling between layers
  79. #droidconGR @jossiwolf Let each platform do what it does best.

  80. Jossi Wolf @jossiwolf From Java To Kotlin Multiplatform