Giter Club home page Giter Club logo

icerockdev / moko-mvvm Goto Github PK

View Code? Open in Web Editor NEW
973.0 973.0 94.0 1.35 MB

Model-View-ViewModel architecture components for mobile (android & ios) Kotlin Multiplatform development

Home Page: https://moko.icerock.dev/

License: Apache License 2.0

Kotlin 89.63% Shell 1.11% Ruby 0.37% Swift 8.66% Objective-C 0.22%
android cocoapod coroutines databinding ios kotlin kotlin-multiplatform kotlin-multiplatform-mobile kotlin-native livedata moko mvvm swift viewmodel

moko-mvvm's Introduction

moko-mvvm
GitHub license Download kotlin-version badge badge badge badge badge badge badge badge badge

Mobile Kotlin Model-View-ViewModel architecture components

This is a Kotlin Multiplatform library that provides architecture components of Model-View-ViewModel for UI applications. Components are lifecycle-aware on Android.

Table of Contents

Features

  • ViewModel - store and manage UI-related data. Interop with Android Architecture Components - on Android it's precisely androidx.lifecycle.ViewModel;
  • LiveData, MutableLiveData, MediatorLiveData - lifecycle-aware reactive data holders with set of operators to transform, merge, etc.;
  • EventsDispatcher - dispatch events from ViewModel to View with automatic lifecycle control and explicit interface of required events;
  • DataBinding, ViewBinding, Jetpack Compose, SwiftUI support - integrate to Android & iOS app with commonly used tools;
  • All Kotlin targets support - core, flow and livedata modules support all Kotlin targets.

Requirements

  • Gradle version 6.8+
  • Android API 16+
  • iOS version 11.0+

Installation

root build.gradle

allprojects {
    repositories {
        mavenCentral()
    }
}

project build.gradle

dependencies {
    commonMainApi("dev.icerock.moko:mvvm-core:0.16.1") // only ViewModel, EventsDispatcher, Dispatchers.UI
    commonMainApi("dev.icerock.moko:mvvm-flow:0.16.1") // api mvvm-core, CFlow for native and binding extensions
    commonMainApi("dev.icerock.moko:mvvm-livedata:0.16.1") // api mvvm-core, LiveData and extensions
    commonMainApi("dev.icerock.moko:mvvm-state:0.16.1") // api mvvm-livedata, ResourceState class and extensions
    commonMainApi("dev.icerock.moko:mvvm-livedata-resources:0.16.1") // api mvvm-core, moko-resources, extensions for LiveData with moko-resources
    commonMainApi("dev.icerock.moko:mvvm-flow-resources:0.16.1") // api mvvm-core, moko-resources, extensions for Flow with moko-resources
    
    // compose multiplatform
    commonMainApi("dev.icerock.moko:mvvm-compose:0.16.1") // api mvvm-core, getViewModel for Compose Multiplatform
    commonMainApi("dev.icerock.moko:mvvm-flow-compose:0.16.1") // api mvvm-flow, binding extensions for Compose Multiplatform
    commonMainApi("dev.icerock.moko:mvvm-livedata-compose:0.16.1") // api mvvm-livedata, binding extensions for Compose Multiplatform

    androidMainApi("dev.icerock.moko:mvvm-livedata-material:0.16.1") // api mvvm-livedata, Material library android extensions
    androidMainApi("dev.icerock.moko:mvvm-livedata-glide:0.16.1") // api mvvm-livedata, Glide library android extensions
    androidMainApi("dev.icerock.moko:mvvm-livedata-swiperefresh:0.16.1") // api mvvm-livedata, SwipeRefreshLayout library android extensions
    androidMainApi("dev.icerock.moko:mvvm-databinding:0.16.1") // api mvvm-livedata, DataBinding support for Android
    androidMainApi("dev.icerock.moko:mvvm-viewbinding:0.16.1") // api mvvm-livedata, ViewBinding support for Android
    
    commonTestImplementation("dev.icerock.moko:mvvm-test:0.16.1") // test utilities
}

Also required export of dependency to iOS framework. For example:

kotlin {
    // export correct artifact to use all classes of library directly from Swift
    targets.withType(org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget::class.java).all {
        binaries.withType(org.jetbrains.kotlin.gradle.plugin.mpp.Framework::class.java).all {
            export("dev.icerock.moko:mvvm-core:0.16.1")
            export("dev.icerock.moko:mvvm-livedata:0.16.1")
            export("dev.icerock.moko:mvvm-livedata-resources:0.16.1")
            export("dev.icerock.moko:mvvm-state:0.16.1")
        }
    }
}

KSwift

For iOS we recommend use moko-kswift with extensions
generation enabled. All LiveData to UIView bindings is extensions for UI elements.

SwiftUI additions

To use MOKO MVVM with SwiftUI set name of your kotlin framework to MultiPlatformLibrary and add dependency to CocoaPods:

pod 'mokoMvvmFlowSwiftUI', :podspec => 'https://raw.githubusercontent.com/icerockdev/moko-mvvm/release/0.16.1/mokoMvvmFlowSwiftUI.podspec'

required export of mvvm-core and mvvm-flow.

Usage

Simple view model

Let’s say we need a screen with a button click counter. To implement it we should:

common

In commonMain we can create a ViewModel like:

class SimpleViewModel : ViewModel() {
    private val _counter: MutableLiveData<Int> = MutableLiveData(0)
    val counter: LiveData<String> = _counter.map { it.toString() }

    fun onCounterButtonPressed() {
        val current = _counter.value
        _counter.value = current + 1
    }
}

And after that integrate the ViewModel on platform the sides.

Android

SimpleActivity.kt:

class SimpleActivity : MvvmActivity<ActivitySimpleBinding, SimpleViewModel>() {
    override val layoutId: Int = R.layout.activity_simple
    override val viewModelVariableId: Int = BR.viewModel
    override val viewModelClass: Class<SimpleViewModel> = SimpleViewModel::class.java

    override fun viewModelFactory(): ViewModelProvider.Factory {
        return createViewModelFactory { SimpleViewModel() }
    }
}

MvvmActivity automatically loads a databinding layout, resolves ViewModel object and sets a databinding variable.
activity_simple.xml:

<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <variable
            name="viewModel"
            type="com.icerockdev.library.sample1.SimpleViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="16dp">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{viewModel.counter.ld}" />

        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:onClick="@{() -> viewModel.onCounterButtonPressed()}"
            android:text="Press me to count" />
    </LinearLayout>
</layout>

iOS

SimpleViewController.swift:

import MultiPlatformLibrary
import MultiPlatformLibraryMvvm

class SimpleViewController: UIViewController {
    @IBOutlet private var counterLabel: UILabel!
    
    private var viewModel: SimpleViewModel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        viewModel = SimpleViewModel()
        
        counterLabel.bindText(liveData: viewModel.counter)
    }
    
    @IBAction func onCounterButtonPressed() {
        viewModel.onCounterButtonPressed()
    }
    
    override func didMove(toParentViewController parent: UIViewController?) {
        if(parent == nil) { viewModel.onCleared() }
    }
}

bindText is an extension from the MultiPlatformLibraryMvvm CocoaPod.

ViewModel with send events to View

Let’s say we need a screen from which we should go to another screen by pressing a button. To implement it we should:

common

class EventsViewModel(
    val eventsDispatcher: EventsDispatcher<EventsListener>
) : ViewModel() {

    fun onButtonPressed() {
        eventsDispatcher.dispatchEvent { routeToMainPage() }
    }

    interface EventsListener {
        fun routeToMainPage()
    }
}

EventsDispatcher is a special class that automatically removes observers from lifecycle and buffers input events while listener is not attached (on the Android side).

Android

EventsActivity.kt:

class EventsActivity : MvvmActivity<ActivityEventsBinding, EventsViewModel>(),
    EventsViewModel.EventsListener {
    override val layoutId: Int = R.layout.activity_events
    override val viewModelVariableId: Int = BR.viewModel
    override val viewModelClass: Class<EventsViewModel> = EventsViewModel::class.java

    override fun viewModelFactory(): ViewModelProvider.Factory {
        return createViewModelFactory { EventsViewModel(eventsDispatcherOnMain()) }
    }

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

        viewModel.eventsDispatcher.bind(
            lifecycleOwner = this,
            listener = this
        )
    }

    override fun routeToMainPage() {
        Toast.makeText(this, "here must be routing to main page", Toast.LENGTH_SHORT).show()
    }
}

eventsDispatcher.bind attaches EventsDispatcher to the lifecycle (in this case - to an activity) to correctly subscribe and unsubscribe, without memory leaks.

We can also simplify the binding of EventsDispatcher with MvvmEventsActivity and EventsDispatcherOwnder. EventsOwnerViewModel.kt:

class EventsOwnerViewModel(
    override val eventsDispatcher: EventsDispatcher<EventsListener>
) : ViewModel(), EventsDispatcherOwner<EventsOwnerViewModel.EventsListener> {

    fun onButtonPressed() {
        eventsDispatcher.dispatchEvent { routeToMainPage() }
    }

    interface EventsListener {
        fun routeToMainPage()
    }
}

EventsOwnderActivity.kt:

class EventsOwnerActivity :
    MvvmEventsActivity<ActivityEventsOwnerBinding, EventsOwnerViewModel, EventsOwnerViewModel.EventsListener>(),
    EventsOwnerViewModel.EventsListener {

    override val layoutId: Int = R.layout.activity_events_owner
    override val viewModelVariableId: Int = BR.viewModel
    override val viewModelClass: Class<EventsOwnerViewModel> = EventsOwnerViewModel::class.java

    override fun viewModelFactory(): ViewModelProvider.Factory {
        return createViewModelFactory { EventsOwnerViewModel(eventsDispatcherOnMain()) }
    }

    override fun routeToMainPage() {
        Toast.makeText(this, "here must be routing to main page", Toast.LENGTH_SHORT).show()
    }
}

iOS

EventsViewController.swift:

import MultiPlatformLibrary
import MultiPlatformLibraryMvvm

class EventsViewController: UIViewController {
    private var viewModel: EventsViewModel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let eventsDispatcher = EventsDispatcher<EventsViewModelEventsListener>(listener: self)
        viewModel = EventsViewModel(eventsDispatcher: eventsDispatcher)
    }
    
    @IBAction func onButtonPressed() {
        viewModel.onButtonPressed()
    }
    
    override func didMove(toParentViewController parent: UIViewController?) {
        if(parent == nil) { viewModel.onCleared() }
    }
}

extension EventsViewController: EventsViewModelEventsListener {
    func routeToMainPage() {
        showAlert(text: "go to main page")
    }
}

On iOS we create an instance of EventsDispatcher with the link to the listener. We shouldn't call bind like on Android (in iOS this method doesn't exist).

ViewModel with validation of user input

class ValidationMergeViewModel() : ViewModel() {
    val email: MutableLiveData<String> = MutableLiveData("")
    val password: MutableLiveData<String> = MutableLiveData("")

    val isLoginButtonEnabled: LiveData<Boolean> = email.mergeWith(password) { email, password ->
        email.isNotEmpty() && password.isNotEmpty()
    }
}

isLoginButtonEnabled is observable email & password LiveData, and in case there are any changes it calls lambda with the newly calculated value.

We can also use one of these combinations:

class ValidationAllViewModel() : ViewModel() {
    val email: MutableLiveData<String> = MutableLiveData("")
    val password: MutableLiveData<String> = MutableLiveData("")

    private val isEmailValid: LiveData<Boolean> = email.map { it.isNotEmpty() }
    private val isPasswordValid: LiveData<Boolean> = password.map { it.isNotEmpty() }
    val isLoginButtonEnabled: LiveData<Boolean> = listOf(isEmailValid, isPasswordValid).all(true)
}

Here we have separated LiveData with the validation flags - isEmailValid, isPasswordValid and combine both to isLoginButtonEnabled by merging all boolean LiveData in the list with on the condition that "all values must be true".

ViewModel for login feature

common

class LoginViewModel(
    override val eventsDispatcher: EventsDispatcher<EventsListener>,
    private val userRepository: UserRepository
) : ViewModel(), EventsDispatcherOwner<LoginViewModel.EventsListener> {
    val email: MutableLiveData<String> = MutableLiveData("")
    val password: MutableLiveData<String> = MutableLiveData("")

    private val _isLoading: MutableLiveData<Boolean> = MutableLiveData(false)
    val isLoading: LiveData<Boolean> = _isLoading.readOnly()

    val isLoginButtonVisible: LiveData<Boolean> = isLoading.not()

    fun onLoginButtonPressed() {
        val emailValue = email.value
        val passwordValue = password.value

        viewModelScope.launch {
            _isLoading.value = true

            try {
                userRepository.login(email = emailValue, password = passwordValue)

                eventsDispatcher.dispatchEvent { routeToMainScreen() }
            } catch (error: Throwable) {
                val message = error.message ?: error.toString()
                val errorDesc = message.desc()

                eventsDispatcher.dispatchEvent { showError(errorDesc) }
            } finally {
                _isLoading.value = false
            }
        }
    }

    interface EventsListener {
        fun routeToMainScreen()
        fun showError(error: StringDesc)
    }
}

viewModelScope is a CoroutineScope field of the ViewModel class with a default Dispatcher - UI on both platforms. All coroutines will be canceled in onCleared automatically.

Android

LoginActivity.kt:

class LoginActivity :
    MvvmEventsActivity<ActivityLoginBinding, LoginViewModel, LoginViewModel.EventsListener>(),
    LoginViewModel.EventsListener {

    override val layoutId: Int = R.layout.activity_login
    override val viewModelVariableId: Int = BR.viewModel
    override val viewModelClass: Class<LoginViewModel> =
        LoginViewModel::class.java

    override fun viewModelFactory(): ViewModelProvider.Factory {
        return createViewModelFactory {
            LoginViewModel(
                userRepository = MockUserRepository(),
                eventsDispatcher = eventsDispatcherOnMain()
            )
        }
    }

    override fun routeToMainScreen() {
        Toast.makeText(this, "route to main page here", Toast.LENGTH_SHORT).show()
    }

    override fun showError(error: StringDesc) {
        Toast.makeText(this, error.toString(context = this), Toast.LENGTH_SHORT).show()
    }
}

activity_login.xml:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="viewModel"
            type="com.icerockdev.library.sample6.LoginViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="16dp">

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="email"
            android:text="@={viewModel.email.ld}" />

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:hint="password"
            android:text="@={viewModel.password.ld}" />

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <Button
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                android:onClick="@{() -> viewModel.onLoginButtonPressed()}"
                android:text="Login"
                app:visibleOrGone="@{viewModel.isLoginButtonVisible.ld}" />

            <ProgressBar
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                app:visibleOrGone="@{viewModel.isLoading.ld}" />
        </FrameLayout>
    </LinearLayout>
</layout>

iOS

LoginViewController.swift:

class LoginViewController: UIViewController {
    @IBOutlet private var emailField: UITextField!
    @IBOutlet private var passwordField: UITextField!
    @IBOutlet private var loginButton: UIButton!
    @IBOutlet private var progressBar: UIActivityIndicatorView!
    
    private var viewModel: LoginViewModel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let eventsDispatcher = EventsDispatcher<LoginViewModelEventsListener>(listener: self)
        viewModel = LoginViewModel(eventsDispatcher: eventsDispatcher,
                                   userRepository: MockUserRepository())
        
        emailField.bindTextTwoWay(liveData: viewModel.email)
        passwordField.bindTextTwoWay(liveData: viewModel.password)
        loginButton.bindVisibility(liveData: viewModel.isLoginButtonVisible)
        progressBar.bindVisibility(liveData: viewModel.isLoading)
    }
    
    @IBAction func onLoginButtonPressed() {
        viewModel.onLoginButtonPressed()
    }
    
    override func didMove(toParentViewController parent: UIViewController?) {
        if(parent == nil) { viewModel.onCleared() }
    }
}

extension LoginViewController: LoginViewModelEventsListener {
    func routeToMainScreen() {
        showAlert(text: "route to main screen")
    }
    
    func showError(error: StringDesc) {
        showAlert(text: error.localized())
    }
}

Samples

Please see more examples in the sample directory.

Set Up Locally

Contributing

All development (both new features and bug fixes) is performed in the develop branch. This way master always contains the sources of the most recently released version. Please send PRs with bug fixes to the develop branch. Documentation fixes in the markdown files are an exception to this rule. They are updated directly in master.

The develop branch is pushed to master on release.

For more details on contributing please see the contributing guide.

We’re hiring a Mobile Developers for our main team in Novosibirsk and remote team with Moscow timezone!

If you like to develop mobile applications, are an expert in iOS/Swift or Android/Kotlin and eager to use Kotlin Multiplatform in production, we'd like to talk to you.

To learn more and apply

License

Copyright 2019 IceRock MAG Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

moko-mvvm's People

Contributors

alex009 avatar anton6tak avatar atchernov avatar darronschall avatar den-gap avatar devtchernov avatar dorofeev avatar exndy avatar idan-fido avatar ignaciocarriondev avatar kovalandrew avatar kramlex avatar lobynya avatar mubashirpa avatar pgochachko avatar tetraquark avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

moko-mvvm's Issues

Specify versions for subspecs dependencies

There's no version for AlamofireImage and you will receive compilation error if you don't set it in your podfile version what support iOS 9.0

MultiPlatformLibraryMVVM.podspec:

    spec.dependency 'MultiPlatformLibrary'

    spec.ios.deployment_target  = '9.0'
    spec.swift_version = '4.2'

    spec.subspec 'Core' do |sp|
      sp.source_files = "mvvm/src/iosMain/swift/Core/**/*.{h,m,swift}"
    end

    spec.subspec 'AlamofireImage' do |sp|
      sp.source_files = "mvvm/src/iosMain/swift/AlamofireImage/**/*.{h,m,swift}"
      sp.dependency 'AlamofireImage'
    end

Temporary workaround for your project:

  pod 'AlamofireImage', '~> 3.5.2' # 4.0.0 have deployment_target = iOS 10.0

trying to share android ViewModels (+Databinding and Navhostfragment)

Hi Alex,

I'm trying to rewrite my android ViewModels into shared ViewModels, but cannot manage to get it working.
I started with LoginViewModel, which is similar to the one in samples. But in my app I use MainActivity with NavHostFragment and Databinding for navigation drawer.
Here is my code. Unfortunately I cannot share the project, but I made gists for all relevant files.

MainActivity.kt
activity_main.xml
nav_header.xml
LoginModel.kt
BaseFragment.kt
LoginFragment.kt
fragment_login.xml

When I clean and build the project, it builds successfully and runs in the emulator.

Then, I see my main activity with login fragment and try to login. When clicking on the login button, LoginModel seems to dispatch the login event which should redirect to the next screen, but nothing happens. I assume I'm doing something wrong when binding the event listener. I was also not sure, whether I have to implement MvvmEventsFragment instead of Fragment() or how I could do it in this case.

 <Button
                android:id="@+id/loginBtn"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/login"
                android:onClick="@{() -> viewModel.onLogin()}"
                android:layout_marginVertical="@dimen/large_margin"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@id/LlLogin" />
fun onLogin() {
        val user = User(username.value, password.value)

        // regardless of login type (cloud or local), validate username and password
        validateUserData(user)?.let {
            eventsDispatcher.dispatchEvent { showError(it) }
            return
        }

        // if cloud login, try login with api
        if (cloudLogin.value) {
            viewModelScope.launch {
                startLoading()
                login(user)?.let { errorString ->
                    eventsDispatcher.dispatchEvent { showError(errorString) }
                    stopLoading()
                    return@launch
                }
                eventsDispatcher.dispatchEvent { navigateToDeviceList(username.value)}
                setLogged(true)
            }
        } else {
            // if local login, validate ip
            validateIpData(url.value)?.let {
                eventsDispatcher.dispatchEvent { showError(it) }
                return
            }
            eventsDispatcher.dispatchEvent { navigateToFavouritesList(url.value)}
            setLogged(true)
        }


    }

I would really appreciate any help.

How to extend MutableLiveData

I am trying to extend MutableLivedata to support SingleLiveEvent for a feature I need.
but the api is restricting how I can conveniently use the class

Cannot find type LiveData in scope

I am trying to set up an initial project. However, in iOS, I get errors in MultiPlatformLibraryMVVM files:

Cannot find type LiveData in scope
Cannot find type UITextFieldBindingKt in scope

It is not clear to me where iOS is supposed to pull in the LiveData from. It does not seem to come in from the MultiPlatformLibrary framework, despite me referencing LiveData in a SimpleViewModel class.

Thanks!

Could not resolve dev.icerock.moko:mvvm:0.6.0

I am combining it into my project after follow basic guidelines for building multi-project using:
https://proandroiddev.com/kotlin-multiplatform-very-beginners-guide-part-1-6419f74afa0f
https://proandroiddev.com/kotlin-multiplatform-very-beginners-guide-part-2-api-d54f7326dc57
I run into strange error of "Unable to find a matching variant of dev.icerock.moko:mvvm:0.6.0" (see below gradle output) thaw I am doing the exact steps in https://github.com/icerockdev/moko-mvvm README

I've added my project gradle files (app and shared) to share location on my Google drive:
https://drive.google.com/file/d/1Iq8IxXFnnnZE-Kpgb9xgbIMcmMvfLDqG/view?usp=sharing

Thank you in advance for the great work.

Best regards,
Gabriel

  • What went wrong:
    Execution failed for task ':Shared:compileKotlinAndroid'.

Could not resolve all files for configuration ':Shared:androidCompileClasspath'.
Could not resolve dev.icerock.moko:mvvm:0.6.0.
Required by:
project :Shared
> Unable to find a matching variant of dev.icerock.moko:mvvm:0.6.0:
- Variant 'android-debugApiElements' capability dev.icerock.moko:mvvm:0.6.0:
- Incompatible attribute:
- Required org.jetbrains.kotlin.platform.type 'jvm' and found incompatible value 'androidJvm'.
- Other attributes:
- Found com.android.build.api.attributes.BuildTypeAttr 'debug' but wasn't required.
- Found com.android.build.api.attributes.VariantAttr 'debug' but wasn't required.
- Found com.android.build.gradle.internal.dependency.AndroidTypeAttr 'Aar' but wasn't required.
- Found org.gradle.status 'release' but wasn't required.
- Required org.gradle.usage 'java-api' and found compatible value 'java-api'.
- Variant 'android-debugRuntimeElements' capability dev.icerock.moko:mvvm:0.6.0:
- Incompatible attribute:
- Required org.jetbrains.kotlin.platform.type 'jvm' and found incompatible value 'androidJvm'.
- Other attributes:
- Found com.android.build.api.attributes.BuildTypeAttr 'debug' but wasn't required.
- Found com.android.build.api.attributes.VariantAttr 'debug' but wasn't required.
- Found com.android.build.gradle.internal.dependency.AndroidTypeAttr 'Aar' but wasn't required.
- Found org.gradle.status 'release' but wasn't required.
- Required org.gradle.usage 'java-api' and found compatible value 'java-runtime'.
- Variant 'android-releaseApiElements' capability dev.icerock.moko:mvvm:0.6.0:
- Incompatible attribute:
- Required org.jetbrains.kotlin.platform.type 'jvm' and found incompatible value 'androidJvm'.
- Other attributes:
- Found com.android.build.api.attributes.BuildTypeAttr 'release' but wasn't required.
- Found com.android.build.api.attributes.VariantAttr 'release' but wasn't required.
- Found com.android.build.gradle.internal.dependency.AndroidTypeAttr 'Aar' but wasn't required.
- Found org.gradle.status 'release' but wasn't required.
- Required org.gradle.usage 'java-api' and found compatible value 'java-api'.
- Variant 'android-releaseRuntimeElements' capability dev.icerock.moko:mvvm:0.6.0:
- Incompatible attribute:
- Required org.jetbrains.kotlin.platform.type 'jvm' and found incompatible value 'androidJvm'.
- Other attributes:
- Found com.android.build.api.attributes.BuildTypeAttr 'release' but wasn't required.
- Found com.android.build.api.attributes.VariantAttr 'release' but wasn't required.
- Found com.android.build.gradle.internal.dependency.AndroidTypeAttr 'Aar' but wasn't required.
- Found org.gradle.status 'release' but wasn't required.
- Required org.gradle.usage 'java-api' and found compatible value 'java-runtime'.
- Variant 'iosArm64-api' capability dev.icerock.moko:mvvm:0.6.0:
- Incompatible attributes:
- Required org.gradle.usage 'java-api' and found incompatible value 'kotlin-api'.
- Required org.jetbrains.kotlin.platform.type 'jvm' and found incompatible value 'native'.
- Other attributes:
- Found org.gradle.status 'release' but wasn't required.
- Found org.jetbrains.kotlin.native.target 'ios_arm64' but wasn't required.
- Variant 'iosX64-api' capability dev.icerock.moko:mvvm:0.6.0:
- Incompatible attributes:
- Required org.gradle.usage 'java-api' and found incompatible value 'kotlin-api'.
- Required org.jetbrains.kotlin.platform.type 'jvm' and found incompatible value 'native'.
- Other attributes:
- Found org.gradle.status 'release' but wasn't required.
- Found org.jetbrains.kotlin.native.target 'ios_x64' but wasn't required.
- Variant 'metadata-api' capability dev.icerock.moko:mvvm:0.6.0:
- Incompatible attributes:
- Required org.gradle.usage 'java-api' and found incompatible value 'kotlin-api'.
- Required org.jetbrains.kotlin.platform.type 'jvm' and found incompatible value 'common'.
- Other attribute:
- Found org.gradle.status 'release' but wasn't required.

Moko Swift extensions are failed to compile

The Swift extensions for Moko LiveData are failed to compile with "Use of undeclared type LiveData" message. It happens because after compiling the shared library the respective class names have "Mvvm" prefix. Moreover, when I change the names to the correct ones, happens that they don't have generics which leads to new compilation errors.

Screen Shot 2020-02-28 at 17 33 50

[Feature Request] Split project into multiple artifacts

I'd propose the following changes in order to better capture the possible needs of consumers:

  1. Since Android is moving towards Flows (StateFlow & SharedFlow), I believe it would make sense to have a separate -livedata artifact
  2. Also with moving towards compose I think the MvvmActivity and binding adapters could also go into a separate artifact
  3. For those who are sharing VM logic & would like to leverage the Android Arch Viewmodel, a lightweight artifact, with the Android VM abstraction, which targets all possible KMP platforms (js, macos, jvm, etc...) would be really beneficial

data binding memory leaks in fragment

There is a problem with memory leaks in fragment: databinding is leaking memory, it is not being cleared on destroy.
The way to fix it is to clear binding in onDestroyView() method.

iOS pod installation problem

Hi!

On iOS, in addition to the Kotlin library add in Podfile

pod 'MultiPlatformLibraryMvvm', :git => 'https://github.com/icerockdev/moko-mvvm.git', :tag => 'release/0.6.0'

Trying to install via cocoa pods and getting an error:

Pre-downloading: MultiPlatformLibraryMvvm from https://github.com/icerockdev/moko-mvvm.git, tag release/0.6.0
[!] Unable to find a specification for MultiPlatformLibrary depended upon by MultiPlatformLibraryMvvm

You have either:

  • out-of-date source repos which you can update with pod repo update or with pod install --repo-update.
  • mistyped the name or version.
  • not added the source repo that hosts the Podspec to your Podfile.

"Pod repo update" doesn't fix the problem, although without it binding LiveData works fine.

Is there something I'm missing?
Thank you

Installation problem - MutableLiveData points to wrong class in commonMain

Hi,

Doing the following steps:

  1. installed Moko-MvvM, adding following dependencies in KMM shared sub-project:
kotlin {
    android()
    val onPhone = System.getenv("SDK_NAME")?.startsWith("iphoneos") ?: false
    if (onPhone) {
        iosArm64("ios")
    } else {
        iosX64("ios")
    }
   
   ....

   sourceSets["commonMain"].dependencies {
         ...
         api("dev.icerock.moko:mvvm:0.8.0")
   }

    sourceSets["androidMain"].dependencies {
        ...
        implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0")
        implementation("android.arch.lifecycle:extensions:2.2.0")
        implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") 
    }
   
}
  1. Updated LiveData and MutableLiveData in an existing ViewModel and ViewBinders (i.e using the .ld method to convert moko livedata to android live data)
  2. Moved the ViewModel from the android app package to the KMM commonMain sub-module.

--- ISSUE

  • Before moving the ViewModel to commonMain
   <com.google.android.material.textfield.TextInputEditText
...
                android:text="@={viewModel.emailIdLiveData.ld()}" // <- works fine
...
        </com.google.android.material.textfield.TextInputLayout>
  • After moving the ViewModel to commonMain
   <com.google.android.material.textfield.TextInputEditText
...
                android:text="@={viewModel.emailIdLiveData.ld()}" // <- error - ld identifier not found
...
        </com.google.android.material.textfield.TextInputLayout>

Any clue or suggestion to solve this would be highly appreciated.

SwiftUI support

If supported - Could you provide an example of how binding would work using SwiftUI?

Crashes in iOS when using Moko's LiveData in callbacks descending to Sqldelight listeners.

CommonDatabase.kt:

fun observe(id: Int, callback: (List<BlablaEntity>) -> Unit) {
        if (id in observers.keys) {
            throw RuntimeException("Already observing id $id")
        } else {
            val listener = object : Query.Listener {
                override fun queryResultsChanged() {
                    callback(
                        database
                            .databaseQueries
                            .findAll { entityId: Long, name: String, blabla: String ->
                                BlablaEntity(
                                    id = entityId,
                                    name = name,
                                    blabla = blabla
                                )
                            }.executeAsList()
                    )
                }
            }

            observers[id] = listener
            database.databaseQueries.findAll().addListener(listener)
        }
    }

Repo.kt:

fun observe(id: Int, onChange: (List<BlablaEntity>) -> Unit) {
        CommonDatabase.observe(id, onChange)
    }

ViewModel.kt:

val statusLiveData: MutableLiveData<Status> = MutableLiveData(Status.Loading)
fun startObserving() {
        val subscriptionId = idCounter.incrementAndGet()
        repo.observe(subscriptionId) { data ->
                statusLiveData.postValue(Status.Success(data))
        }
    }

When using startObserving() method from iOS, the app crashes with the following:

Instances of kotlin.Error, kotlin.RuntimeException and subclasses aren't propagated from Kotlin to Objective-C/Swift.
Other exceptions can be propagated as NSError if method has or inherits @throws annotation.
Uncaught Kotlin exception: kotlin.native.concurrent.InvalidMutabilityException: mutation attempt of frozen kotlin.Array@1ca1508

How to unsubscribe from live data in RecyclerView adapters?

I am trying to update a progress bar on the first item of a RecyclerView using live data, however it doesn't seem to unsubscribe properly so other items also get included into it. Any way to make it unsubscribe properly?

   if (position == 0) {
            ExoPlayerManager.currentPlaytime.addObserver {
                b.progress.max = ExoPlayerManager.exoPlayer.duration.toInt()
                b.progress.progress = it.toInt()
            }
        } else {
            ExoPlayerManager.currentPlaytime.removeObserver { }
            b.icon.setImageDrawable(ContextCompat.getDrawable(b.root.context, R.drawable.ic_music_note))
            b.root.alpha = 0.5f
            b.progress.progress = 0
            b.progress.max = 0
        }

The live data resides here:

object ExoPlayerManager {
    lateinit var exoPlayer: SimpleExoPlayer
    private lateinit var context: Context
    private val handler = Handler()
    val currentPlaytime: MutableLiveData<Long> = MutableLiveData(initialValue = 0)
    //....

The first item updates properly:

image

... but so does the last item:

image

add public Dispatchers.UI which used in viewModelScope

in some cases like updatePushToken we should use coroutines without ViewModel. and in this case required worked Dispatcher, but UIDispatcher is private in library.
i suggest add:
expect val Dispatchers.UI: CoroutineDispatcher

on android:
actual val Dispatchers.UI: CoroutineDispatcher get() = Dispatchers.Main

on ios:
actual val Dispatchers.UI: CoroutineDispatcher get() = UIDispatcher()

later, when coroutines multithreading will be released, we just deprecate Dispatchers.UI and on iOS pass Dispatchers.Main instead UIDispatcher

question about unit testing MutableLiveData

Hello
I am start writing multi platform code using moko-mvvm. Now I want to write unit tests for my common logic which is using MutableLiveData. At first run I encounter the error:
java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked
which seems to be related to android.
Searching a bit i found this as a solution. Problem is it requires InstantTaskExecutorRule which is part of android platform and thus is not accessible in common code. I know I can move my test to android source set and it will goes well but I think its more clean to hold common tests in common source sets. So does anyone have a suggestion how to fix this?

In iOS bindImageUrl cannot be specified placeholder

In bindImageUrl extension for ImageView, default image is hardcoded and I can't use custom image for placeholder.

public func bindImageUrl(liveData: LiveData<NSString>) {
    setUrl(url: liveData.value)
    liveData.addObserver { [weak self] url in
      self?.setUrl(url: url)
    }
  }

  private func setUrl(url: NSString?) {
    setImageFromUrlString(
      urlString: url as? String,
      defaultImage: #imageLiteral(resourceName: "no_photo_con"),
      completion: nil
    )
  }

Build error

Hi. When building I have this error

Could not determine the dependencies of task ':shared:compileKotlinMetadata'.

Couldn't resolve metadata artifact for ModuleDependencyIdentifier(groupId=dev.icerock.moko, moduleId=resources) in configuration ':shared:iosArm64CompileKlibraries'

Screen Shot 2020-12-07 at 23 03 10

Error Cannot access ViewModel

I get some error: Cannot access 'dev.icerock.moko.mvvm.viewmodel.ViewModel' which is a supertype of 'net.compoza.deactivator.mpp.model.ListViewModel'. Check your module classpath for missing or conflicting dependencies.

ViewModel class:
Снимок экрана 2020-09-29 в 12 32 27

Fragment code:
Снимок экрана 2020-09-29 в 12 32 07

shared build.gradle:
Снимок экрана 2020-09-29 в 12 36 11
Снимок экрана 2020-09-29 в 12 36 20

I use:
Android Studio: 4.0.1
Kotlin: 1.4.10
gradle: 6.6
build.gradle: 4.0.1
And block Android() in shared build.gradle.

Add commands queue to iOS EventsDispatcher

To support case of immediate dispatches after EventsDispatcher pass to ViewModel:

class EnterPhoneScreen(
) : WidgetScreen<Args.Empty>() {
    override fun createContentWidget(): Widget<WidgetSize.Const<SizeSpec.AsParent, SizeSpec.AsParent>> {
        val viewModel = getViewModel {
            createViewModel(createEventsDispatcher()).apply { start() } // HERE RUN START, where event call immediate
        }
        viewModel.eventsDispatcher.listen(this, this) // HERE SUBSCRIPTION

        // ...
    }
}

flatMap creates extra shadow LiveData's

In current implementation of flatMap extension of the LiveData class there is an extra "shadow" LiveData objects which passes wrong data to an observers and makes extra calls for the transformation callbacks.

Problems with installation

User encountered the following issues in android module:

Cannot access class 'dev.icerock.moko.mvvm.dispatcher.EventsDispatcher'. Check your module classpath for missing or conflicting dependencies

and

Type parameter bound for T in operator fun <T : ViewModel!> get(p0: Class<T!>): T
 is not satisfied: inferred type HomeViewModel! is not a subtype of ViewModel!

first issue is strange and should be investigated.

second - fixes by

dependencies {
    androidMainImplementation("androidx.lifecycle:lifecycle-extensions:2.0.0")
}

just in install section from readme.

both issue was fixed by user when in android module was added:
implementation("dev.icerock.moko:mvvm:0.6.0")

Use MutableLiveData with Model

I try use MutableLiveData with Model, but it not work for me.
My Model:
Снимок экрана 2020-07-01 в 16 31 03

My Screen Model:
Снимок экрана 2020-07-01 в 16 30 44
Снимок экрана 2020-07-01 в 16 30 53

And use in Screen:
Снимок экрана 2020-07-01 в 16 31 40

And I dont understand how use MutableLiveData type - Int. I try get value like this:
Снимок экрана 2020-07-01 в 16 31 50

But its not work:
Снимок экрана 2020-07-01 в 16 32 02

And one more question, how use MutableLiveData type Array.

Add optionals for new iOS view bindings

with new ios view bindings required write

let errorText: LiveData<StringDesc> = viewModel.state.error().map { $0 as? StringDesc ?? StringDesc.Raw(string: "") } as! LiveData<StringDesc>
errorLabel.bindText(liveData: errorText)

because optionals not supported

Implement sample

  • viewmodel creating (with android viewModelProvider)
  • livedata read & write, mapping, flatMap, merge
  • events dispatching
  • suspend functions in coroutine scope
  • state management in livedata

Can I have a custom name for library?

Hi!

I'm trying to integrate moko-mvvm for iOS. Unfortunately, integration instructions are not clear for me. In commercial project we have Kotlin Native Multiplatform engine for several years. On Kotlin side everything is fine, but in iOS I'm unsuccessfully trying to just install moko and have its classes in Xcode. Is it strictly required to have a name "MultiPlatformLibrary" and "mpp-library" for builded framework? This how it is in example iOS project:
https://github.com/icerockdev/moko-mvvm/blob/master/sample/ios-app/Podfile

No matching variant of dev.icerock.moko:mvvm:0.8.0 was found

Hi, I add moko-mvvm to multiplatform project, but when start build I get error:
no matching variant of dev.icerock.moko:mvvm:0.8.0 was found

I use kotlin: 1.4.10
build gradle: 4.0.1
gradle: 6.6
Android Studio: 4.0.1

build.gradle:
add maven url to allProjects
1

Add implementation from common gradle
build.gradle.kts:
2
3

ViewModel without any errors:
4

But I get error:
5

Update readme

  • logo (?)
  • description with goals
  • gif with clear example in action
  • features - viewmodel creating, livedata subscriptions & update, lifecycle aware events dispatching, state management, livedata operations, coroutine scope on viewmodels
  • simple usage - simple viewmodel with livedata & one event dispatch
  • installation steps
  • detailed usage - livedata mappings, merge, execute suspend functions in coroutine scopes
  • how to contribute
  • license block

IOS - Alamofireimage build Error, probably wrong mpp config.

I try to using moko-mvvm and Mobule Multiplatform gradle plugin, but my ios project still remains error, did i missing another config ? I also done setting up pod config which require MultiPlatformLibrary on my /ios/PodFile

Before alamofire error i'm also facing livedata error not found, but after adding alamofire dependency to podFile it leading to alamofire error like i said above.

Please check my public repository since i'm using it for learning purposes.
Also i hope somebody make a pull request so i can read and learn whats wrong on my configuration.

Add MutableLiveData empty constructor.

Thank you for nice mpp mvvm solution!
It will be great if MutableLiveData will have empty constructor. It will let developer to handle case when there is no initial data. And then post updates when some process will be done.

Separate library to multiple artifacts

It would be nice to see the new moko-viewmodel or moko-mvvm-viewmodel library. Without databinding support for declarative UI - Jetpack Compose.

And the moko-mvvm library can be updated by composing from moko-viewmodel, moko-livedata (for the future, as an example)

ViewBinding support out of box

VBActivity:

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import dev.icerock.moko.mvvm.viewmodel.ViewModel

abstract class VBActivity<VB : Any, VM : ViewModel> : AppCompatActivity() {

    protected lateinit var viewModel: VM
    protected lateinit var binding: VB

    protected abstract val viewModelClass: Class<VM>

    protected abstract fun viewModelFactory(): ViewModelProvider.Factory
    protected abstract fun viewBinding(layoutInflater: LayoutInflater): VB
    protected abstract fun VB.root(): View

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

        viewModel = ViewModelProvider(this, viewModelFactory())[viewModelClass]
        binding = viewBinding(layoutInflater)

        setContentView(binding.root())
    }
}

VBEventsActivity:

import android.os.Bundle
import dev.icerock.moko.mvvm.dispatcher.EventsDispatcherOwner
import dev.icerock.moko.mvvm.viewmodel.ViewModel

abstract class VBEventsActivity<VB : Any, VM, Listener : Any> :
    VBActivity<VB, VM>() where VM: ViewModel, VM : EventsDispatcherOwner<Listener> {

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

        @Suppress("UNCHECKED_CAST")
        viewModel.eventsDispatcher.bind(this, this as Listener)
    }
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.