kotlin mvvm+dataBinding+retrofit2+Arouter等BaseActivity、BaseFragment、BaseDialogFragment基类封装

Related tags

Utility kotlin-mvvm
Overview

kotlin-mvvm

kotlin mvvm+dataBinding+retrofit2+ARouter等BaseActivity、BaseFragment、BaseDialogFragment基类封装

Android开发项目基本使用框架,封装了各类组件,在基类实现了沉浸式状态栏,可以自己更改颜色,更高效全能开发框架

框架所有功能

里面封装各种组件:

viewmodel封装

apiService(hostUrl: String = ZjConfig.base_url): T = RetrofitManager.instance.apiService(T::class.java, hostUrl) /** * 公用的网络请求发起的操作 * @param observable 发起请求的被观察着 * @param observer 观察着回调 */ fun doNetRequest(observable: Observable >, observer: BaseObserver ) { val subscribeWith = observable .compose(ResponseTransformer.instance.handleResult()) .compose(SchedulerProvider.instance.applySchedulers()) .subscribeWith(observer) subscribeWith.getDisposable()?.let { addSubscribe(it) } } ">
open class BaseViewModel : ViewModel() {

    var pageIndex = 1
    var pageSize = 10
    private var isAddDisposable = false
    private val mCompositeDisposable = CompositeDisposable()

    //添加网络请求到CompositeDisposable
    private fun addSubscribe(disposable: Disposable) {
        mCompositeDisposable.also {
            Log.e("--okhttp--", "disposable is add")
            isAddDisposable = true
            it.add(disposable)
        }
    }

    override fun onCleared() {
        //解除网络请求
        mCompositeDisposable.also {
            if (isAddDisposable) {
                Log.e("--okhttp--", "disposable is clear")
                isAddDisposable = false
                mCompositeDisposable.clear()
            }
        }
    }

    /**
     * 实例化网络请求
     * hostUrl 域名, 默认ZjConfig.base_url,需要修改传入新的域名(新的每次都传)
     */
    inline fun 
      
        apiService(hostUrl: String = ZjConfig.base_url): T =
        RetrofitManager.instance.apiService(T::class.java, hostUrl)

    /**
     * 公用的网络请求发起的操作
     * @param observable 发起请求的被观察着
     * @param observer 观察着回调
     */
    fun 
       
         doNetRequest(observable: Observable
        
         >, observer: BaseObserver
         
          ) {
        val subscribeWith = observable
            .compose(ResponseTransformer.instance.handleResult())
            .compose(SchedulerProvider.instance.applySchedulers())
            .subscribeWith(observer)
        subscribeWith.getDisposable()?.let { addSubscribe(it) }
    }

         
        
       
      

发起网络请求

单独创建接口类

@POST(ApiManager.APPLOGIN_URL)
fun login(@Body body: RequestBody): Observable
   
    
     >

    
   

在viewmodle中调用即可(BaseObserver(true)显示loading加载框)

doNetRequest(apiService
   
    ().login(BaseMapToBody.convertMapToBody(map)), object : BaseObserver
    
     (true) {

                override fun onISuccess(message: String, response: LoginBean) {
                    sid.set(response.bussData)
                    ToastUtils.showShort("code=${message}")
                }

                override fun onIError(e: ApiException) {
                    sid.set(e.message)
                    ToastUtils.showShort("code=${e.message}")
                }
            })

    
   

RelativeItemView 一个item,左右文字图片一个控件完美使用

">

  

属性

riv_leftImg 左边图片
riv_leftImgWidth 左边图片宽度 
riv_leftImgHeight 左边图片高度 
riv_leftText 左边文字 
riv_leftTextColor 左边文字颜色
riv_leftTextSize 左边文字_字体大小 
riv_leftTextPaddingLeft 左边文字_据左边距离
riv_leftTextDrawable 左边文字_drawableLeft图片 
riv_leftTextDrawableRight 左边文字_drawableLeft图片是否在右边 
riv_leftTextDrawablePadding 左边文字_drawablePadding
riv_leftTextDrawableTint 左边文字_drawableTint
riv_leftTextStyle 左边文字_加粗属性 
riv_editText Edit文字 
riv_editTextColor Edit文字颜色 
riv_editTextHint Edit的hint文字 
riv_editTextHintColor Edit的hint文字颜色
riv_editTextSize Edit文字_字体大小 
riv_editTextEnabled Edit是否可以编辑 
riv_editTextBackground Edit背景 
riv_editTextGravity Edit的gravity位置 
riv_editTextMarginLeft Edit文字_MarginLeft
riv_editTextMarginRight Edit文字_MarginRight 
riv_editTextPaddingLeft Edit文字_PaddingLeft 
riv_editTextPaddingRight Edit文字_PaddingRight 
riv_editTextStyle Edit文字_加粗属性
riv_rightText 右边文字
riv_rightTextHint 右边文字hint
riv_rightTextHintColor 右边文字hint颜色
riv_rightTextColor 右边文字颜色
riv_rightTextSize 右边文字_字体大小
riv_rightTextPaddingRight 右边文字_据左边距离 
riv_rightTextDrawable 右边文字_drawableRight图片 
riv_rightTextDrawablePadding 右边文字_drawablePadding
riv_rightTextDrawableTint 右边文字_drawableTint 
riv_rightTextStyle 右边文字_加粗属性 
riv_driverShow 下划线是否显示
riv_driverColor 下划线颜色 
riv_driverHeight 下划线高度
riv_driverMarginLeft 下划线距离左边距离 
riv_driverMarginRight 下划线距离右边距离
riv_driverMarginHorizontal 下划线距离左右边距离 

TitleBarView 通用标题栏封装

">

  

属性

tb_centerText 中间标题
tb_centerTextColor 中间标题文字颜色 
tb_centerTextSize 中间标题文字_字体大小
tb_rightText 右边文字 
tb_rightTextColor 右边文字颜色 
tb_rightTextSize 右边文字_字体大小
tb_leftText 左边文字
tb_leftTextColor 左边文字颜色 
tb_leftTextSize 左边文字_字体大小 
tb_leftImageDrawable 左边图片
tb_rightImageDrawable 右边图片 
tb_rightImageDrawable2 右边图片2
tb_rightImage2_marginRight 右边图片2_距离右边距离 
tb_divider 底部分割线 
tb_titleBarHeight TitleBar高度
tb_titleBarBackground TitleBar背景色 

普通类继承 BaseActivity(BaseFragment、BaseDialogFragment 同理)

class PictureActivity(override val layoutRes: Int = R.layout.activity_picture) : BaseActivity
   
    () {

    //可以不写(使用默认)
    override val viewModel: BaseViewModel = MyViewModel()
    
    override fun initView(savedInstanceState: Bundle?) {
      
    }
}

   

BaseActivity封装

, type: Int) { if (DoubleUtils.isFastDoubleClick()) return startActivityForResult(Intent(this, classActivity), type) } /** * 携带数据的页面跳转 * * @param url 对应组建的名称 (“/mine/setting”) * navigation的第一个参数***必须是Activity***,第二个参数则是RequestCode */ fun startActivityForResult(url: String, bundle: Bundle, type: Int) { if (DoubleUtils.isFastDoubleClick()) return ARouter.getInstance().build(url).with(bundle).navigation(this, type) } //不使用路由跳转 fun startActivityForResult(classActivity: Class<*>, bundle: Bundle, type: Int) { if (DoubleUtils.isFastDoubleClick()) return startActivityForResult(Intent(this, classActivity).putExtras(bundle), type) } /** * 语言适配 */ override fun attachBaseContext(newBase: Context?) { super.attachBaseContext(newBase?.let { LanguageUtil().attachBaseContext(it) }) } /** * 点击edittext以外区域隐藏软键盘 */ override fun dispatchTouchEvent(ev: MotionEvent): Boolean { if (ev.action == MotionEvent.ACTION_DOWN) { val v = currentFocus if (isShouldHideInput(v, ev)) { val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm?.hideSoftInputFromWindow(v!!.windowToken, 0) } return super.dispatchTouchEvent(ev) } // 必不可少,否则所有的组件都不会有TouchEvent了 try { if (window.superDispatchTouchEvent(ev)) { return true } } catch (e: Exception) { } return onTouchEvent(ev) } private fun isShouldHideInput(v: View?, event: MotionEvent): Boolean { if (v != null && v is EditText) { val leftTop = intArrayOf(0, 0) // 获取输入框当前的location位置 v.getLocationInWindow(leftTop) val left = leftTop[0] val top = leftTop[1] val bottom = top + v.getHeight() val right = left + v.getWidth() return !(event.x > left && event.x < right && event.y > top && event.y < bottom) } return false } private fun getLoadingDialog() { loadingDialog ?: also { loadingDialog = LoadingDialog(this) } } /** * 显示加载dialog */ fun showLoading() { try { getLoadingDialog() loadingDialog?.show() } catch (e: Exception) { e.printStackTrace() } } /** * 结束dialog */ fun dismissLoading() { try { loadingDialog?.let { if (it.isShowing) it.dismiss() } } catch (e: Exception) { e.printStackTrace() } } ">
abstract class BaseActivity
   
     : RxAppCompatActivity(), JumpActivity {

    lateinit var binding: BINDING
    private val isNotAddActivityList = "is_add_activity_list" //是否加入到activity的list,管理
    private var mApplication: BaseApplication? = null
    private var loadingDialog: LoadingDialog? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mApplication = application as BaseApplication
        //添加到栈管理
        val isNotAdd = intent.getBooleanExtra(isNotAddActivityList, false)
        synchronized(BaseActivity::class.java) {
            if (!isNotAdd) mApplication?.getActivityList()?.add(this)
        }
        initViewDataBinding()
        initImmersionBars()
        //初始化组件
        ARouter.getInstance().inject(this)
        initView(savedInstanceState)
    }

    private fun initViewDataBinding() {
        if (layoutRes != 0) binding = DataBindingUtil.setContentView(this, layoutRes)
        val mViewModel = ViewModelProvider(this, ViewModelFactory(viewModel))[viewModel::class.java]
        //允许设置变量的值而不反映
        binding?.setVariable(viewModelId, mViewModel)
        //支持LiveData绑定xml,数据改变,UI自动会更新
        binding?.lifecycleOwner = this
    }

    @get:LayoutRes
    abstract val layoutRes: Int
    open val viewModel: BaseViewModel = NormalViewModel()
    open val viewModelId = 0
    abstract fun initView(savedInstanceState: Bundle?)

    override fun onDestroy() {
        super.onDestroy()
        synchronized(BaseActivity::class.java) { mApplication?.getActivityList()?.remove(this) }
        binding?.unbind()
    }

    /**
     * 页面跳转
     *
     * @param url 对应组建的名称 (“/mine/setting”)
     * navigation的第一个参数***必须是Activity***,第二个参数则是RequestCode
     */
    fun startActivityForResult(url: String, type: Int) {
        if (DoubleUtils.isFastDoubleClick()) return
        ARouter.getInstance().build(url).navigation(this, type)
    }

    //不使用路由跳转
    fun startActivityForResult(classActivity: Class<*>, type: Int) {
        if (DoubleUtils.isFastDoubleClick()) return
        startActivityForResult(Intent(this, classActivity), type)
    }

    /**
     * 携带数据的页面跳转
     *
     * @param url 对应组建的名称  (“/mine/setting”)
     * navigation的第一个参数***必须是Activity***,第二个参数则是RequestCode
     */
    fun startActivityForResult(url: String, bundle: Bundle, type: Int) {
        if (DoubleUtils.isFastDoubleClick()) return
        ARouter.getInstance().build(url).with(bundle).navigation(this, type)
    }

    //不使用路由跳转
    fun startActivityForResult(classActivity: Class<*>, bundle: Bundle, type: Int) {
        if (DoubleUtils.isFastDoubleClick()) return
        startActivityForResult(Intent(this, classActivity).putExtras(bundle), type)
    }

    /**
     * 语言适配
     */
    override fun attachBaseContext(newBase: Context?) {
        super.attachBaseContext(newBase?.let { LanguageUtil().attachBaseContext(it) })
    }

    /**
     * 点击edittext以外区域隐藏软键盘
     */
    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        if (ev.action == MotionEvent.ACTION_DOWN) {
            val v = currentFocus
            if (isShouldHideInput(v, ev)) {
                val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
                imm?.hideSoftInputFromWindow(v!!.windowToken, 0)
            }
            return super.dispatchTouchEvent(ev)
        }
        // 必不可少,否则所有的组件都不会有TouchEvent了
        try {
            if (window.superDispatchTouchEvent(ev)) {
                return true
            }
        } catch (e: Exception) {
        }
        return onTouchEvent(ev)
    }

    private fun isShouldHideInput(v: View?, event: MotionEvent): Boolean {
        if (v != null && v is EditText) {
            val leftTop = intArrayOf(0, 0)
            // 获取输入框当前的location位置
            v.getLocationInWindow(leftTop)
            val left = leftTop[0]
            val top = leftTop[1]
            val bottom = top + v.getHeight()
            val right = left + v.getWidth()
            return !(event.x > left && event.x < right && event.y > top && event.y < bottom)
        }
        return false
    }

    private fun getLoadingDialog() {
        loadingDialog ?: also { loadingDialog = LoadingDialog(this) }
    }

    /**
     * 显示加载dialog
     */
    fun showLoading() {
        try {
            getLoadingDialog()
            loadingDialog?.show()
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    /**
     * 结束dialog
     */
    fun dismissLoading() {
        try {
            loadingDialog?.let { if (it.isShowing) it.dismiss() }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

   

BaseFragment封装

abstract class BaseFragment
   
     : RxFragment(), JumpActivity, ImmersionOwner {

    lateinit var binding: BINDING
    private var rootView: View? = null
    private lateinit var mContext: Context
    private var loadingDialog: LoadingDialog? = null

    //ImmersionBar代理类
    private val mImmersionProxy = ImmersionProxy(this)

    override fun initImmersionBar() {
        initImmersionBars()
    }

    override fun setUserVisibleHint(isVisibleToUser: Boolean) {
        super.setUserVisibleHint(isVisibleToUser)
        mImmersionProxy.isUserVisibleHint = isVisibleToUser
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        mContext = context
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mImmersionProxy.onCreate(savedInstanceState)
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        ARouter.getInstance().inject(this)
        if (null == rootView) { //如果缓存中有rootView则直接使用
            initViewDataBinding(inflater, container)
            this.rootView = binding.root;
        } else {
            rootView?.let {
                it.parent?.let { it2 -> (it2 as ViewGroup).removeView(it) }
            }
        }
        return rootView
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        //在OnCreate方法中调用下面方法,然后再使用线程,就能在uncaughtException方法中捕获到异常
        initView(savedInstanceState)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        mImmersionProxy.onActivityCreated(savedInstanceState)
    }

    private fun initViewDataBinding(inflater: LayoutInflater, container: ViewGroup?) {
        if (layoutRes != 0) binding =
            DataBindingUtil.inflate(inflater, layoutRes, container, false)
        val mViewModel = ViewModelProvider(this, ViewModelFactory(viewModel))[viewModel::class.java]
        //允许设置变量的值而不反映
        binding?.setVariable(viewModelId, mViewModel)
        //支持LiveData绑定xml,数据改变,UI自动会更新
        binding?.lifecycleOwner = this
    }

    @get:LayoutRes
    abstract val layoutRes: Int
    open val viewModel: BaseViewModel = NormalViewModel()
    open val viewModelId = 0
    abstract fun initView(savedInstanceState: Bundle?)

    open fun getRootView(): View? = rootView

    override fun onResume() {
        super.onResume()
        mImmersionProxy.onResume()
    }

    override fun onPause() {
        super.onPause()
        mImmersionProxy.onPause()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        //为rootView做缓存,在viewpager中使用fragment时可以提升切换流畅度
        rootView?.let {
            it.parent?.let { it2 -> (it2 as ViewGroup).removeView(it) }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        mImmersionProxy.onDestroy()
        binding?.unbind()
    }

    /**
     * 页面跳转
     *
     * @param url 对应组建的名称 (“/mine/setting”)
     * navigation的第一个参数***必须是Activity***,第二个参数则是RequestCode
     */
    fun startActivityForResult(url: String, type: Int) {
        if (DoubleUtils.isFastDoubleClick()) return
        val intent = Intent(context, getDestination(url))
        startActivityForResult(intent, type)
    }

    //不使用路由跳转
    fun startActivityForResult(classActivity: Class<*>, type: Int) {
        if (DoubleUtils.isFastDoubleClick()) return
        startActivityForResult(Intent(activity, classActivity), type)
    }

    /**
     * 携带数据的页面跳转
     *
     * @param url 对应组建的名称  (“/mine/setting”)
     * navigation的第一个参数***必须是Activity***,第二个参数则是RequestCode
     */
    fun startActivityForResult(url: String, bundle: Bundle, type: Int) {
        if (DoubleUtils.isFastDoubleClick()) return
        val intent = Intent(context, getDestination(url))
        intent.putExtras(bundle)
        startActivityForResult(intent, type)
    }

    //不使用路由跳转
    fun startActivityForResult(classActivity: Class<*>, bundle: Bundle, type: Int) {
        if (DoubleUtils.isFastDoubleClick()) return
        startActivityForResult(Intent(activity, classActivity).putExtras(bundle), type)
    }

    /**
     * 由于ARouter不支持Fragment startActivityForResult(),需要获取跳转的Class
     * 根据路径获取具体要跳转的class
     */
    private fun getDestination(url: String): Class<*> {
        val postcard = ARouter.getInstance().build(url)
        LogisticsCenter.completion(postcard)
        return postcard.destination
    }

    override fun onHiddenChanged(hidden: Boolean) {
        super.onHiddenChanged(hidden)
        mImmersionProxy.onHiddenChanged(hidden)
    }

    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)
        mImmersionProxy.onConfigurationChanged(newConfig)
    }

    /**
     * 懒加载,在view初始化完成之前执行
     * On lazy after view.
     */
    override fun onLazyBeforeView() {}

    /**
     * 懒加载,在view初始化完成之后执行
     * On lazy before view.
     */
    override fun onLazyAfterView() {}

    /**
     * Fragment用户可见时候调用
     * On visible.
     */
    override fun onVisible() {}

    /**
     * Fragment用户不可见时候调用
     * On invisible.
     */
    override fun onInvisible() {}

    /**
     * 是否可以实现沉浸式,当为true的时候才可以执行initImmersionBar方法
     * Immersion bar enabled boolean.
     *
     * @return the boolean
     */
    override fun immersionBarEnabled(): Boolean = true

    private fun getLoadingDialog() {
        loadingDialog ?: also { loadingDialog = LoadingDialog(context!!) }
    }

    /**
     * 显示加载dialog
     */
    fun showLoading() {
        try {
            getLoadingDialog()
            loadingDialog?.show()
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    /**
     * 结束dialog
     */
    fun dismissLoading() {
        try {
            loadingDialog?.let { if (it.isShowing) it.dismiss() }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

   
You might also like...
Dagger Hilt, MVP Moxy, Retrofit, Kotlin coroutine, Sealed class

Dagger Hilt, MVP Moxy, Retrofit, Kotlin coroutine, Sealed class

Item Helper For Kotlin

Item Helper For Kotlin

 Bar Service Kotlin Client
Bar Service Kotlin Client

A simple starter service client written in Kotlin against generated models (protos)A simple starter service client written in Kotlin against generated models (protos)

Kotlin validation with a focus on readability
Kotlin validation with a focus on readability

kommodus Kotlin validation with a focus on readability import com.github.kommodus.constraints.* import com.github.kommodus.Validation data class Test

Kotlin to Dart compiler
Kotlin to Dart compiler

Dotlin is a Kotlin to Dart compiler. The aim is to integrate Kotlin as a language into the Dart ecosystem, combing best of both worlds: The Kotlin lan

Modern Kotlin version of com.example.semitop7.FireTVStyle keyboard

ftv-style-keyboard Modern Kotlin version of com.example.semitop7.FireTVStyle keyboard Manual activation on FireTV via adb shell: adb shell ime enable

A Kotlin-based testing/scraping/parsing library providing the ability to analyze and extract data from HTML
A Kotlin-based testing/scraping/parsing library providing the ability to analyze and extract data from HTML

A Kotlin-based testing/scraping/parsing library providing the ability to analyze and extract data from HTML (server & client-side rendered). It places particular emphasis on ease of use and a high level of readability by providing an intuitive DSL. It aims to be a testing lib, but can also be used to scrape websites in a convenient fashion.

PubSub - 使用 Kotlin Coroutines 实现的 Local Pub/Sub、Event Bus、Message Bus

PubSub 使用 Kotlin Coroutines 实现的 Local Pub/Sub、Event Bus、Message Bus 下载 将它添加到项目的

KDoctor - A command-line tool that helps to set up the environment for Kotlin Multiplatform Mobile app development

KDoctor is a command-line tool that helps to set up the environment for Kotlin Multiplatform Mobile app development.

Owner
奋斗中的骚年
一个只会敲代码默默付出的码农
奋斗中的骚年
Remove the dependency of compiled kotlin on kotlin-stdlib

Dekotlinify This project aims to take compiled Kotlin Java bytecode (compiled by the standard Kotlin compiler) and remove all references to the Kotlin

Joseph Burton 10 Nov 29, 2022
gRPC and protocol buffers for Android, Kotlin, and Java.

Wire “A man got to have a code!” - Omar Little See the project website for documentation and APIs. As our teams and programs grow, the variety and vol

Square 3.9k Dec 31, 2022
A DSL to handle soft keyboard visibility change event written in Kotlin.

About A DSL to handle soft keyboard visibility change event written in Kotlin. How to use? Step 1. Add it in your root build.gradle at the end of repo

Vinícius Oliveira 17 Jan 7, 2023
Kotlin matrix class which supports determinant, inverse matrix, matmul, etc.

Kotrix is a set of classes that helps people dealing with linear algebra in Kotlin.

Kanguk Lee 5 Dec 8, 2022
Fuzzy string matching for Kotlin (JVM, native, JS, Web Assembly) - port of Fuzzy Wuzzy Python lib

FuzzyWuzzy-Kotlin Fuzzy string matching for Kotlin (JVM, iOS) - fork of the Java fork of of Fuzzy Wuzzy Python lib. For use in on JVM, Android, or Kot

WillowTree, LLC 54 Nov 8, 2022
🐫🐍🍢🅿 Multiplatform Kotlin library to convert strings between various case formats including Camel Case, Snake Case, Pascal Case and Kebab Case

KaseChange Multiplatform Kotlin library to convert strings between various case formats Supported Case Formats SCREAMING_SNAKE_CASE snake_case PascalC

PearX Team 67 Dec 30, 2022
Multiplaform kotlin library for calculating text differences. Based on java-diff-utils, supports JVM, JS and native targets.

kotlin-multiplatform-diff This is a port of java-diff-utils to kotlin with multiplatform support. All credit for the implementation goes to original a

Peter Trifanov 51 Jan 3, 2023
Multi-module, Kotlin, MVI, Compose, Hilt, Navigation Component, Use-cases, Room, Retrofit

Work in progress Multi-module demo app that gets data from dota2 api. API https://docs.opendota.com/ Players by rank (GET) https://api.opendota.com/ap

Mitch Tabian 321 Dec 27, 2022
recompose is a tool for converting Android layouts in XML to Kotlin code using Jetpack Compose.

recompose is a tool for converting Android layouts in XML to Kotlin code using Jetpack Compose.

Sebastian Kaspari 565 Jan 2, 2023
Markdown renderer for Kotlin Compose Multiplatform (Android, Desktop)

Markdown renderer for Kotlin Compose Multiplatform (Android, Desktop)

Mike Penz 129 Jan 1, 2023