Kotlin / kmp-production-sample

This is an open-source, mobile, cross-platform application built with Kotlin Multiplatform Mobile. It's a simple RSS reader, and you can download it from the App Store and Google Play. It's been designed to demonstrate how KMM can be used in real production projects.
https://kotlinlang.org/lp/mobile/
MIT License
2.01k stars 224 forks source link

basic react.js app with shared module dependency (as commonjs) #47

Closed ybonnetain closed 2 years ago

ybonnetain commented 2 years ago

Hello,

This PR would add a React.js application to display one RSS feed (screenshot: media/web.png)

I have done that because I think native JS UI is still more appealing to web developers. That being said we should be able to benefit from code sharing for business logic, validation rules, data acces, persistency logic and so on ..

I don't know if this would be of interest to you.. It is probably light years from being mergeable but, if you get a chance to review I would love some feedbacks as they would probably teach me things I don't know.

Basically I added a jsMain source set to the module. Then I added a Kotlin/JS module to the project. On top of that I used create-react-app to create a simple react.js application (webApp/app). The shared module is installed with NPM / compiled with webpack (exported as commonjs using Kotlin/JS IR compiler)

Networking in jsMain with Ktor

internal fun jsHttpClient(withLog: Boolean) = HttpClient(Js) // ..

XML parsing is done in the jsMain with DOMParser

internal class JsFeedParser : FeedParser {
    override suspend fun parse(sourceUrl: String, xml: String, isDefault: Boolean): Feed {
        val parser = DOMParser()
        val feed = parser.parseFromString(xml, "text/xml")
        // ...
    }
}

Then I added a function to create the reader for JS as function extensions seem not to be supported yet.

fun createRssReader(withLog: Boolean) = RssReader(
    FeedLoader(
        jsHttpClient(withLog),
        JsFeedParser()
    ),
    FeedStorage(
        JsSettings(),
        Json {
            ignoreUnknownKeys = true
            isLenient = true
            encodeDefaults = false
        }
    )
).also {
    if (withLog) Napier.base(DebugAntilog())
}

Initialize reader and store with Koin (jsMain)

@ExperimentalJsExport
fun initKoin() {
    val deps = module {
        single { createRssReader(true) }
        single { FeedStore(get()) }
    }

    startKoin {
        modules(deps)
    }
}

A view model object in the Kotlin/JS module, mainly because sealed classed representing actions are not exportable to JS. Also used to init the Koin context.

@ExperimentalJsExport
@JsExport
object RssReaderJsViewModel : KoinComponent {
    private val mainScope = MainScope()
    private var store : FeedStore

    init {
        initKoin()
        store = get()
    }

    @Suppress("unused")
    fun cancel() {
        mainScope.cancel()
    }

    @Suppress("unused")
    fun refreshFeeds() {
        store.dispatch(FeedAction.Refresh(true))
    }

    @Suppress("unused")
    fun observeStore(callback: (state: FeedState) -> Unit) {
        mainScope.launch {
            store.observeState().collect {
                callback(it)
            }
        }
    }
}

Import commonjs module in React app

import SharedRssRead from 'RssReader-shared'

Expose the state with React Context API

const RssFeedContext = createContext()
const useRssFeedContext = () => useContext(RssFeedContext)

Bind to component lifecycle

useEffect(() => {
    viewModel.observeStore(onStateUpdate)
    viewModel.refreshFeeds()
    return () => viewModel.cancel()
  }, [viewModel])

Use in UI component

const { loading, posts } = useRssFeedContext();

Thank you for providing this project anyway :) I am a redux fan !

Kind regards

terrakok commented 2 years ago

Oh! Thank you for web part! I will review asap :)

terrakok commented 2 years ago

I can't start web project:

yarn run v1.22.17
$ react-scripts start
node:internal/modules/cjs/loader:488
      throw e;
      ^

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './lib/tokenize' is not defined by "exports" in /Users/.../kmm-production-sample/webApp/node_modules/postcss-safe-parser/node_modules/postcss/package.json
    at new NodeError (node:internal/errors:371:5)
    at throwExportsNotFound (node:internal/modules/esm/resolve:416:9)
    at packageExportsResolve (node:internal/modules/esm/resolve:669:3)
    at resolveExports (node:internal/modules/cjs/loader:482:36)
    at Function.Module._findPath (node:internal/modules/cjs/loader:522:31)
    at Function.Module._resolveFilename (node:internal/modules/cjs/loader:919:27)
    at Function.Module._load (node:internal/modules/cjs/loader:778:27)
    at Module.require (node:internal/modules/cjs/loader:999:19)
    at require (node:internal/modules/cjs/helpers:102:18)
    at Object.<anonymous> (/Users/.../kmm-production-sample/webApp/node_modules/postcss-safe-parser/lib/safe-parser.js:1:17) {
  code: 'ERR_PACKAGE_PATH_NOT_EXPORTED'
}

Node.js v17.0.1
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
ybonnetain commented 2 years ago

I can't start web project:

yarn run v1.22.17
$ react-scripts start
node:internal/modules/cjs/loader:488
      throw e;
      ^

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './lib/tokenize' is not defined by "exports" in /Users/.../kmm-production-sample/webApp/node_modules/postcss-safe-parser/node_modules/postcss/package.json
    at new NodeError (node:internal/errors:371:5)
    at throwExportsNotFound (node:internal/modules/esm/resolve:416:9)
    at packageExportsResolve (node:internal/modules/esm/resolve:669:3)
    at resolveExports (node:internal/modules/cjs/loader:482:36)
    at Function.Module._findPath (node:internal/modules/cjs/loader:522:31)
    at Function.Module._resolveFilename (node:internal/modules/cjs/loader:919:27)
    at Function.Module._load (node:internal/modules/cjs/loader:778:27)
    at Module.require (node:internal/modules/cjs/loader:999:19)
    at require (node:internal/modules/cjs/helpers:102:18)
    at Object.<anonymous> (/Users/.../kmm-production-sample/webApp/node_modules/postcss-safe-parser/lib/safe-parser.js:1:17) {
  code: 'ERR_PACKAGE_PATH_NOT_EXPORTED'
}

Node.js v17.0.1
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

Ouch this seems related to Node version and dependency package https://github.com/facebook/create-react-app/issues/11565

I was able to reproduce the issue with Node 17

I used NVM (Node version manager) https://github.com/nvm-sh/nvm curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash

nvm install 17.0.0
nvm use 17.0.0
yarn start // build crashes !

How it worked for me was to switch to the latest v16

nvm use 16.8.0
yarn start // build Okay

Would you mind managing multiple Node versions on your dev machine to test ?

I'm just gonna add an engines attribute to the package.json to indicate required node version (for now)

ybonnetain commented 2 years ago

yarn

install all dependencies from npm registry + compile and install shared module

yarn start

Runs the app in the development mode.\ Open http://localhost:3000 to view it in the browser. The page will reload if you make edits.\ You will also see any lint errors in the console.

yarn run compile

Compiles the MPP shared module, install it and start app. Run this command to appy changes made to the shared module inside the JS app.

Accessing a feed without cross origin control allowed

More commands in webApp/README.md

terrakok commented 2 years ago

Shared code looks OK but I would like to have more shiny Web UI :)

image

it is RSS feed from here: https://vas3k.ru/rss/

It would be great if you used Compose for Web... 🥳 https://www.jetbrains.com/lp/compose-mpp/

ybonnetain commented 2 years ago

Do you know if there is a spec for RSS ? It looks like every one of them has its own way of embedding the content 🤯

What do you think about this :

This would show even more ways of integrating KMM. We can also add a Compose for Desktop version maybe..

Would that be ok ? Or you'd rather get rid of the React.js client app ? I think using JS is still more appealing for a lot of web developers and this could be an enabler for KMM adoption in production projects.

terrakok commented 2 years ago

https://www.rssboard.org/rss-specification

Yes, it would be awesome to finish React UI and add Compose variant too 👍 (btw, I think to try create Desktop App)

terrakok commented 2 years ago

FYI: https://github.com/Kotlin/kmm-production-sample/tree/shared-compose

just run ./gradlew :desktopApp:run

terrakok commented 2 years ago

merged to master!

ybonnetain commented 2 years ago

merged to master!

I have been away for a while, sorry