Slide 1

Slide 1 text

Jossi Wolf @jossiwolf From Java To Kotlin Multiplatform

Slide 2

Slide 2 text

#droidconGR @jossiwolf # Kotlin Multiplatform

Slide 3

Slide 3 text

#droidconGR @jossiwolf Android Web Desktop iOS

Slide 4

Slide 4 text

#droidconGR @jossiwolf data class RabbitEntity( val name: String, val colour: Colour )

Slide 5

Slide 5 text

#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 } }

Slide 6

Slide 6 text

#droidconGR @jossiwolf Android Web Desktop iOS

Slide 7

Slide 7 text

#droidconGR @jossiwolf Android Web Desktop iOS Common

Slide 8

Slide 8 text

#droidconGR @jossiwolf Common

Slide 9

Slide 9 text

#droidconGR @jossiwolf Common

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

#droidconGR @jossiwolf $ gradle compileJS $ "Running on JS" $ gradle compileJVM $ "Running on JVM"

Slide 15

Slide 15 text

#droidconGR @jossiwolf # Converting

Slide 16

Slide 16 text

#droidconGR @jossiwolf 1.Identify your components

Slide 17

Slide 17 text

#droidconGR @jossiwolf

Slide 18

Slide 18 text

#droidconGR @jossiwolf UI

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

#droidconGR @jossiwolf class MainPanel extends JFrame

Slide 21

Slide 21 text

#droidconGR @jossiwolf class AnimationPanel extends JPanel

Slide 22

Slide 22 text

#droidconGR @jossiwolf class CycleEdit extends JPanel

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

#droidconGR @jossiwolf actual typealias JFrame = swing.JFrame //JVM

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

#droidconGR @jossiwolf //JS actual class JFrame { }

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

#droidconGR @jossiwolf Separating your concerns is important!

Slide 31

Slide 31 text

#droidconGR @jossiwolf Your UI should not be shared.

Slide 32

Slide 32 text

#droidconGR @jossiwolf

Slide 33

Slide 33 text

#droidconGR @jossiwolf 2. Convert your models

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

#droidconGR @jossiwolf Models CycleModel CycleSetModel HyperSpline Easing Interpolator MonotoneSpline LinearInterpolator Oscillator Data Domain

Slide 36

Slide 36 text

#droidconGR @jossiwolf 2.1 Convert your data models

Slide 37

Slide 37 text

#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(); }

Slide 38

Slide 38 text

#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 }

Slide 39

Slide 39 text

#droidconGR @jossiwolf Your data models are probably the easiest to start with!

Slide 40

Slide 40 text

#droidconGR @jossiwolf …but what about logic?

Slide 41

Slide 41 text

#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 }

Slide 42

Slide 42 text

#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) } } }

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

#droidconGR @jossiwolf * Make use of the Collections API

Slide 45

Slide 45 text

#droidconGR @jossiwolf * Make sure you don‘t depend on Java Math APIs!

Slide 46

Slide 46 text

#droidconGR @jossiwolf Models CycleModel CycleSetModel HyperSpline Easing Interpolator MonotoneSpline LinearInterpolator Oscillator Data Domain

Slide 47

Slide 47 text

#droidconGR @jossiwolf 2.2 Convert your domain models

Slide 48

Slide 48 text

#droidconGR @jossiwolf CycleModel.java 614 LOC :O

Slide 49

Slide 49 text

#droidconGR @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); } }

Slide 50

Slide 50 text

#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 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) }

Slide 51

Slide 51 text

#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) }

Slide 52

Slide 52 text

#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 mMode; JButton delete, add; public JSlider mPos, mPeriod, mAmp, mOff; ... }

Slide 53

Slide 53 text

#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; }

Slide 54

Slide 54 text

#droidconGR @jossiwolf Separating your concerns is important!

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

#droidconGR @jossiwolf # Refactoring really really messy code

Slide 57

Slide 57 text

#droidconGR @jossiwolf Model UI

Slide 58

Slide 58 text

#droidconGR @jossiwolf Model UI State (User) Actions

Slide 59

Slide 59 text

#droidconGR @jossiwolf Model

Slide 60

Slide 60 text

#droidconGR @jossiwolf Model Repository

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

#droidconGR @jossiwolf Model Repository

Slide 64

Slide 64 text

#droidconGR @jossiwolf Model Repository Presentation

Slide 65

Slide 65 text

#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 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) }

Slide 66

Slide 66 text

#droidconGR @jossiwolf Extract instead of removing.

Slide 67

Slide 67 text

#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) { ... } }

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

#droidconGR @jossiwolf # Decoupling your Presentation Layer

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

#droidconGR @jossiwolf data class CycleViewState( val isMedian: Boolean, val positionMin: Int, val positionMax: Int )

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

#droidconGR @jossiwolf class CycleActivity: AppCompatActivity() { }

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

#droidconGR @jossiwolf Let each platform do what it does best.

Slide 80

Slide 80 text

Jossi Wolf @jossiwolf From Java To Kotlin Multiplatform