Let's talking about KMP

Meet the Team

Harry Hsu

Sr. SWE Engineer

林宇軒

Sr. SWE Engineer

Who am I

Calvin Huang

 

終於讓我有機會做這件事了

這是我家貓咪

名字是黑妞,可愛的男孩子 ♂

Today, we won't be discussing the traditional methods

為什麼大家都想要 Code once run everywhere?

神代的人類語言統一

創造了無數輝煌

人們建造了巴別塔嘗試達到神的境界而被懲罰

混亂的語言讓世界不再統一

無法再創過往的榮耀

全都圍繞在一個字上:榨取產能

But how we can achieve that goal?

Take a glance at UI

Credit: https://www.learnui.design/blog/ios-vs-android-app-ui-design-complete-guide.html

Look into the sample code

struct LoginView: View {

    @StateObject var viewModel = LoginViewModel()

    var body: some View {
        VStack {
            Image("STREAMMARK")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(height: 40)
                .padding(.all, 24)

            Text("Welcome to Stream Chat")
                .font(.title)
                .padding(.all, 8)

            Text("Select a user to try the iOS SDK:")
                .font(.body)
                .padding(.all, 8)
                .padding(.bottom, 16)

            List(viewModel.demoUsers) { user in
                Button {
                    viewModel.demoUserTapped(user)
                } label: {
                    DemoUserView(user: user)
                }
                .padding(.vertical, 4)
                .animation(nil)
            }
            .listStyle(.plain)

            Spacer()
        }
        .overlay(
            viewModel.loading ? ProgressView() : nil
        )
    }
}
@Composable
    private fun LoginScreen() {
        Column(
            modifier = Modifier.fillMaxSize(),
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            Spacer(modifier = Modifier.height(32.dp))

            Icon(
                modifier = Modifier.size(width = 80.dp, height = 40.dp),
                painter = painterResource(id = R.drawable.ic_stream),
                contentDescription = null,
                tint = ChatTheme.colors.primaryAccent,
            )

            Spacer(modifier = Modifier.height(28.dp))

            Text(
                modifier = Modifier.padding(horizontal = 16.dp),
                text = stringResource(R.string.login_screen_title),
                fontSize = 22.sp,
                fontWeight = FontWeight.Bold,
                color = ChatTheme.colors.textHighEmphasis,
            )

            Spacer(modifier = Modifier.height(12.dp))

            LazyColumn(
                modifier = Modifier
                    .fillMaxWidth()
                    .weight(1f),
            ) {
                items(items = LoginUsers.createUsers()) { loginUser ->
                    UserItem(loginUser = loginUser)

                    DividerItem()
                }
            }
        }
    }

Look into the sample code

struct LoginView: View {

    @StateObject var viewModel = LoginViewModel()

    var body: some View {
        VStack {
            Image("STREAMMARK")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(height: 40)
                .padding(.all, 24)

            Text("Welcome to Stream Chat")
                .font(.title)
                .padding(.all, 8)

            Text("Select a user to try the iOS SDK:")
                .font(.body)
                .padding(.all, 8)
                .padding(.bottom, 16)

            List(viewModel.demoUsers) { user in
                Button {
                    viewModel.demoUserTapped(user)
                } label: {
                    DemoUserView(user: user)
                }
                .padding(.vertical, 4)
                .animation(nil)
            }
            .listStyle(.plain)

            Spacer()
        }
        .overlay(
            viewModel.loading ? ProgressView() : nil
        )
    }
}
@Composable
    private fun LoginScreen() {
        Column(
            modifier = Modifier.fillMaxSize(),
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            Spacer(modifier = Modifier.height(32.dp))

            Icon(
                modifier = Modifier.size(width = 80.dp, height = 40.dp),
                painter = painterResource(id = R.drawable.ic_stream),
                contentDescription = null,
                tint = ChatTheme.colors.primaryAccent,
            )

            Spacer(modifier = Modifier.height(28.dp))

            Text(
                modifier = Modifier.padding(horizontal = 16.dp),
                text = stringResource(R.string.login_screen_title),
                fontSize = 22.sp,
                fontWeight = FontWeight.Bold,
                color = ChatTheme.colors.textHighEmphasis,
            )

            Spacer(modifier = Modifier.height(12.dp))

            LazyColumn(
                modifier = Modifier
                    .fillMaxWidth()
                    .weight(1f),
            ) {
                items(items = LoginUsers.createUsers()) { loginUser ->
                    UserItem(loginUser = loginUser)

                    DividerItem()
                }
            }
        }
    }

Look into the sample code

同場加映 - React???

mport React, { useState, useEffect } from 'react';

const LoginView = () => {
  const [viewModel, setViewModel] = useState(new LoginViewModel());
  const [loading, setLoading] = useState(viewModel.loading);

  useEffect(() => {
    // Assuming you have a way to update loading status in your view model
    const handleLoadingChange = () => setLoading(viewModel.loading);
    viewModel.onLoadingChange = handleLoadingChange;

    return () => {
      viewModel.onLoadingChange = null;
    };
  }, [viewModel]);

  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
      <img
        src="path_to_STREAMMARK_image"
        style={{ height: '40px', aspectRatio: 'fit', padding: '24px' }}
        alt="STREAMMARK"
      />

      <h1 style={{ padding: '8px' }}>Welcome to Stream Chat</h1>
      <p style={{ padding: '8px', paddingBottom: '16px' }}>Select a user to try the iOS SDK:</p>

      <div style={{ width: '100%' }}>
        {viewModel.demoUsers.map(user => (
          <button key={user.id} onClick={() => viewModel.demoUserTapped(user)} style={{ padding: '4px', width: '100%', textAlign: 'left' }}>
            <DemoUserView user={user} />
          </button>
        ))}
      </div>

      {loading && <p>Loading...</p>}
    </div>
  );
};

export default LoginView;

And...architecture

MVP

MVVM

MVI

總結起來為一個字:極度相似

Then things took a different turn

Design

matters

Then things took a different turn

Native matters

How Flutter draws?

The Dart code that paints Flutter’s visuals is compiled into native code, which uses Skia (or, in future, Impeller) for rendering.

How Flutter draws?

Let's take a brief dive into the mechanism

How ReactNative draws?

the React Native renderer communicates with the host platform to mount host views on the screen (create, insert, update or delete of host views) and it listens for events that are generated by the user on the host platform.

How ReactNative draws?

Let's take a brief dive into the mechanism

This is the most common scenario where most of the render pipeline happens on JavaScript thread.

So what is the limitation?

 

Be cautious of ReactNative performance

JS frame rate (JavaScript thread)

For most React Native applications, your business logic will run on the JavaScript thread.

...

Any animations controlled by JavaScript would appear to freeze during that time. If anything takes longer than 100ms, the user will feel it.

So what is the limitation?

 

It's better in Flutter

Then you know what?

 

Native UI always works better

FPS:

Native UI always works better

At the very least, don't let it bother you too much.

Ultimately, it doesn't really matter. 60FPS works well for everyone, and most people won't notice if you drop a few frames here and there... except maybe for professional gamers

Due to some other limitations...

Both Flutter and ReactNative

If you want it to appear like a native app, you'll need to create that effect on your own.

Of course the original UI experiences...?

Flutter does the awesome effort to save your time

Due to some other limitations...

Both Flutter and ReactNative

And it still some issues, though

All come to Kotlin Multiplatform mobile

Native Implementation

Shared Module

Base on the flexibility

來說點隨處可見、且無聊的事情吧

  • 發包給外部廠商實作,溝通來回之下工期被壓縮
  • 案子草創期沒有訂下明確的 spec、加上廠商沒有問清楚就實作,導致做完之後時間更短還因為做錯重改
  • 連鎖反應導致工期不足,雪上加霜的是開發人員熟悉的開發模式過時某些功能直接抄 stackoverflow
  • 資安檢測修改項目不熟悉,工期不足 trial & error,但不知道為什麼復測會過

這時候又需要將外包的 code 接回來繼續做下去怎麼辦!?

Shared module

5%

Base on the flexibility

We can integrate the legacy in easy way

...haha

來說點隨處可見、且無聊的事情吧

The different core concept compared to other cross-platform solutions

Provide a cross-platform UI solution

Built the app on top on of that

Try to figure out the all common behaviors and adapt them to shared module

Get limited since all build on top of fundation UI framework

Provide a way to share the logic (perhaps data)

Extract common logic across projects

Discuss if anything else needs to be placed at shared module

Feel free to leave or stay whenever you want

Provide a cross-platform UI solution

Built the app on top on of that

Try to figure out the all common behaviors and adapt them to shared module

Get limited since all build on top of fundation UI framework

極端例子: NFC 靠卡儲值實作

因為 UI 用跨平台方案

讀不同平台的文件,找出 API 共同的行為

根據跨平台方案的接口整合

Potential Risk

Provide a way to share the logic (perhaps data)

Extract common logic across projects

Discuss if anything else needs to be placed at shared module

Feel free to leave or stay whenever you want

極端例子: NFC 靠卡儲值實作

基於不同平台分別實作

討論是不是要將相似的接口整合進 shared module

- 交換完全 native,

- 實作接口,讓 shared-

流程後拋回 UI 連動處理

module 處理交換

Total autonomy

因為 UI 用跨平台方案

讀不同平台的文件,找出 API 共同的行為

根據跨平台方案的接口整合

Different priority, different form of the team

Flutter/ReactNative...etc first

Native platform second

Different priority, different form of the team

Native platform first

KMP second

基於不同平台分別實作

討論是不是要將相似的接口整合進 shared module

- 交換完全 native,

- 實作接口,讓 shared-

流程後拋回 UI 連動處理

module 處理交換

Different priority, different form of the team

November 1, 2023

Kotlin Multiplatofm is stable

Exciting roadmap ahead

Exciting roadmap ahead

Our tips :)

Before

Our tips :)

Before

After

Get in touch

QA time

https://app.sli.do/event/aSqr53HcJnCNPacBdvCCGa

Recurting

https://www.cakeresume.com/companies/i-pass

Code

By Calvin Huang

Code

  • 277