diff --git a/tools/templates/RiotXFeature/globals.xml.ftl b/tools/templates/RiotXFeature/globals.xml.ftl new file mode 100644 index 0000000000..7cafd80483 --- /dev/null +++ b/tools/templates/RiotXFeature/globals.xml.ftl @@ -0,0 +1,6 @@ +<?xml version="1.0"?> +<globals> + <#include "root://activities/common/common_globals.xml.ftl" /> + <global id="resOut" value="${resDir}" /> + <global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" /> +</globals> diff --git a/tools/templates/RiotXFeature/recipe.xml.ftl b/tools/templates/RiotXFeature/recipe.xml.ftl new file mode 100644 index 0000000000..88160fd0f3 --- /dev/null +++ b/tools/templates/RiotXFeature/recipe.xml.ftl @@ -0,0 +1,37 @@ +<?xml version="1.0"?> +<#import "root://activities/common/kotlin_macros.ftl" as kt> +<recipe> + + <instantiate from="root/res/layout/fragment.xml.ftl" + to="${escapeXmlAttribute(resOut)}/layout/${escapeXmlAttribute(fragmentLayout)}.xml" /> + <open file="${escapeXmlAttribute(resOut)}/layout/${fragmentLayout}.xml" /> + + <#if createActivity> + <instantiate from="root/src/app_package/Activity.kt.ftl" + to="${escapeXmlAttribute(srcOut)}/${activityClass}.kt" /> + <open file="${escapeXmlAttribute(srcOut)}/${activityClass}.kt" /> + </#if> + + <instantiate from="root/src/app_package/Fragment.kt.ftl" + to="${escapeXmlAttribute(srcOut)}/${fragmentClass}.kt" /> + <open file="${escapeXmlAttribute(srcOut)}/${fragmentClass}.kt" /> + + <instantiate from="root/src/app_package/ViewModel.kt.ftl" + to="${escapeXmlAttribute(srcOut)}/${viewModelClass}.kt" /> + <open file="${escapeXmlAttribute(srcOut)}/${viewModelClass}.kt" /> + + <instantiate from="root/src/app_package/ViewState.kt.ftl" + to="${escapeXmlAttribute(srcOut)}/${viewStateClass}.kt" /> + <open file="${escapeXmlAttribute(srcOut)}/${viewStateClass}.kt" /> + + <instantiate from="root/src/app_package/Action.kt.ftl" + to="${escapeXmlAttribute(srcOut)}/${actionClass}.kt" /> + <open file="${escapeXmlAttribute(srcOut)}/${actionClass}.kt" /> + + <#if createViewEvents> + <instantiate from="root/src/app_package/ViewEvents.kt.ftl" + to="${escapeXmlAttribute(srcOut)}/${viewEventsClass}.kt" /> + <open file="${escapeXmlAttribute(srcOut)}/${viewEventsClass}.kt" /> + </#if> + +</recipe> diff --git a/tools/templates/RiotXFeature/root/res/layout/fragment.xml.ftl b/tools/templates/RiotXFeature/root/res/layout/fragment.xml.ftl new file mode 100644 index 0000000000..539c40f3f9 --- /dev/null +++ b/tools/templates/RiotXFeature/root/res/layout/fragment.xml.ftl @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/rootConstraintLayout" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <TextView + android:id="@+id/message" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="${fragmentClass}" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"/> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/tools/templates/RiotXFeature/root/src/app_package/Action.kt.ftl b/tools/templates/RiotXFeature/root/src/app_package/Action.kt.ftl new file mode 100644 index 0000000000..7492907e6c --- /dev/null +++ b/tools/templates/RiotXFeature/root/src/app_package/Action.kt.ftl @@ -0,0 +1,5 @@ +package ${escapeKotlinIdentifiers(packageName)} + +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class ${actionClass}: VectorViewModelAction \ No newline at end of file diff --git a/tools/templates/RiotXFeature/root/src/app_package/Activity.kt.ftl b/tools/templates/RiotXFeature/root/src/app_package/Activity.kt.ftl new file mode 100644 index 0000000000..fdac319482 --- /dev/null +++ b/tools/templates/RiotXFeature/root/src/app_package/Activity.kt.ftl @@ -0,0 +1,49 @@ +package ${escapeKotlinIdentifiers(packageName)} + +import android.content.Context +import android.content.Intent +import androidx.appcompat.widget.Toolbar +import im.vector.riotx.R +import im.vector.riotx.core.extensions.addFragment +import im.vector.riotx.core.platform.ToolbarConfigurable +import im.vector.riotx.core.platform.VectorBaseActivity + +//TODO: add this activity to manifest +class ${activityClass} : VectorBaseActivity(), ToolbarConfigurable { + + companion object { + + <#if createFragmentArgs> + private const val EXTRA_FRAGMENT_ARGS = "EXTRA_FRAGMENT_ARGS" + + fun newIntent(context: Context, args: ${fragmentArgsClass}): Intent { + return Intent(context, ${activityClass}::class.java).apply { + putExtra(EXTRA_FRAGMENT_ARGS, args) + } + } + <#else> + fun newIntent(context: Context): Intent { + return Intent(context, ${activityClass}::class.java) + } + </#if> + } + + override fun getLayoutRes() = R.layout.activity_simple + + override fun initUiAndData() { + if (isFirstCreation()) { + <#if createFragmentArgs> + val fragmentArgs: ${fragmentArgsClass} = intent?.extras?.getParcelable(EXTRA_FRAGMENT_ARGS) + ?: return + addFragment(R.id.simpleFragmentContainer, ${fragmentClass}::class.java, fragmentArgs) + <#else> + addFragment(R.id.simpleFragmentContainer, ${fragmentClass}::class.java) + </#if> + } + } + + override fun configure(toolbar: Toolbar) { + configureToolbar(toolbar) + } + +} diff --git a/tools/templates/RiotXFeature/root/src/app_package/Fragment.kt.ftl b/tools/templates/RiotXFeature/root/src/app_package/Fragment.kt.ftl new file mode 100644 index 0000000000..df69078d9d --- /dev/null +++ b/tools/templates/RiotXFeature/root/src/app_package/Fragment.kt.ftl @@ -0,0 +1,47 @@ +package ${escapeKotlinIdentifiers(packageName)} + +import android.os.Bundle +<#if createFragmentArgs> +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize +import com.airbnb.mvrx.args +</#if> +import android.view.View +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorBaseFragment +import javax.inject.Inject + +<#if createFragmentArgs> +@Parcelize +data class ${fragmentArgsClass}() : Parcelable +</#if> + +//TODO: add this fragment into FragmentModule +class ${fragmentClass} @Inject constructor( + private val viewModelFactory: ${viewModelClass}.Factory +) : VectorBaseFragment(), ${viewModelClass}.Factory by viewModelFactory { + + <#if createFragmentArgs> + private val fragmentArgs: ${fragmentArgsClass} by args() + </#if> + private val viewModel: ${viewModelClass} by fragmentViewModel() + + override fun getLayoutResId() = R.layout.${fragmentLayout} + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + // Initialize your view, subscribe to viewModel... + } + + override fun onDestroyView() { + super.onDestroyView() + // Clear your view, unsubscribe... + } + + override fun invalidate() = withState(viewModel) { state -> + //TODO + } + +} diff --git a/tools/templates/RiotXFeature/root/src/app_package/ViewEvents.kt.ftl b/tools/templates/RiotXFeature/root/src/app_package/ViewEvents.kt.ftl new file mode 100644 index 0000000000..4d7d2ee450 --- /dev/null +++ b/tools/templates/RiotXFeature/root/src/app_package/ViewEvents.kt.ftl @@ -0,0 +1,5 @@ +package ${escapeKotlinIdentifiers(packageName)} + +import im.vector.riotx.core.platform.VectorViewEvents + +sealed class ${viewEventsClass} : VectorViewEvents \ No newline at end of file diff --git a/tools/templates/RiotXFeature/root/src/app_package/ViewModel.kt.ftl b/tools/templates/RiotXFeature/root/src/app_package/ViewModel.kt.ftl new file mode 100644 index 0000000000..f4090b40e6 --- /dev/null +++ b/tools/templates/RiotXFeature/root/src/app_package/ViewModel.kt.ftl @@ -0,0 +1,44 @@ +package ${escapeKotlinIdentifiers(packageName)} + +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.riotx.core.platform.VectorViewModel + +<#if createViewEvents> +<#else> +import im.vector.riotx.core.platform.EmptyViewEvents +</#if> + +class ${viewModelClass} @AssistedInject constructor(@Assisted initialState: ${viewStateClass}) + <#if createViewEvents> + : VectorViewModel<${viewStateClass}, ${actionClass}, ${viewEventsClass}>(initialState) { + <#else> + : VectorViewModel<${viewStateClass}, ${actionClass}, EmptyViewEvents>(initialState) { + </#if> + + @AssistedInject.Factory + interface Factory { + fun create(initialState: ${viewStateClass}): ${viewModelClass} + } + + companion object : MvRxViewModelFactory<${viewModelClass}, ${viewStateClass}> { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: ${viewStateClass}): ${viewModelClass}? { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } + + override fun handle(action: ${actionClass}) { + //TODO + } + +} diff --git a/tools/templates/RiotXFeature/root/src/app_package/ViewState.kt.ftl b/tools/templates/RiotXFeature/root/src/app_package/ViewState.kt.ftl new file mode 100644 index 0000000000..55e1f5f549 --- /dev/null +++ b/tools/templates/RiotXFeature/root/src/app_package/ViewState.kt.ftl @@ -0,0 +1,5 @@ +package ${escapeKotlinIdentifiers(packageName)} + +import com.airbnb.mvrx.MvRxState + +data class ${viewStateClass}() : MvRxState \ No newline at end of file diff --git a/tools/templates/RiotXFeature/template.xml b/tools/templates/RiotXFeature/template.xml new file mode 100644 index 0000000000..33d2edfc70 --- /dev/null +++ b/tools/templates/RiotXFeature/template.xml @@ -0,0 +1,121 @@ +<?xml version="1.0"?> +<template + format="5" + revision="1" + name="RiotX Feature" + minApi="19" + minBuildApi="19" + description="Creates a new activity and a fragment with view model, view state and actions"> + + <category value="New Vector" /> + <formfactor value="Mobile" /> + + <parameter + id="createActivity" + name="Create host activity" + type="boolean" + default="true" + help="If true, you will have a host activity" /> + + <parameter + id="activityClass" + name="Activity Name" + type="string" + constraints="class|unique|nonempty" + visibility="createActivity" + default="MainActivity" + help="The name of the activity class to create" /> + + <parameter + id="fragmentClass" + name="Fragment Name" + type="string" + constraints="class|unique|nonempty" + suggest="${underscoreToCamelCase(classToResource(activityClass))}Fragment" + default="MainFragment" + help="The name of the fragment class to create" /> + + <parameter + id="createFragmentArgs" + name="Create fragment Args" + type="boolean" + default="false" + help="If true, you will have a fragment args" /> + + <parameter + id="fragmentArgsClass" + name="Fragment Args" + type="string" + constraints="class|unique|nonempty" + visibility="createFragmentArgs" + suggest="${underscoreToCamelCase(classToResource(fragmentClass))}Args" + default="MainArgs" + help="The name of the fragment args to create" /> + + <parameter + id="fragmentLayout" + name="Fragment Layout Name" + type="string" + constraints="layout|unique|nonempty" + suggest="fragment_${classToResource(fragmentClass)}" + default="main_fragment" + help="The name of the layout to create for the fragment" /> + + <parameter + id="viewModelClass" + name="ViewModel Name" + type="string" + constraints="class|unique|nonempty" + suggest="${underscoreToCamelCase(classToResource(fragmentClass))}ViewModel" + default="MainViewModel" + help="The name of the view model class to create" /> + + <parameter + id="actionClass" + name="Action Name" + type="string" + constraints="class|unique|nonempty" + suggest="${underscoreToCamelCase(classToResource(fragmentClass))}Action" + default="MainAction" + help="The name of the action class to create" /> + + <parameter + id="viewStateClass" + name="ViewState Name" + type="string" + constraints="class|unique|nonempty" + suggest="${underscoreToCamelCase(classToResource(fragmentClass))}ViewState" + default="MainViewState" + help="The name of the ViewState class to create" /> + + <parameter + id="createViewEvents" + name="Create ViewEvents" + type="boolean" + default="false" + help="If true, you will have a view events" /> + + <parameter + id="viewEventsClass" + name="ViewEvents Class" + type="string" + constraints="class|unique|nonempty" + visibility="createViewEvents" + + suggest="${underscoreToCamelCase(classToResource(fragmentClass))}ViewEvents" + default="MainViewEvents" + help="The name of the view events to create" /> + + + + <parameter + id="packageName" + name="Package name" + type="string" + constraints="package" + default="com.mycompany.myapp" /> + + <globals file="globals.xml.ftl" /> + <execute file="recipe.xml.ftl" /> + +</template> diff --git a/tools/templates/configure.sh b/tools/templates/configure.sh new file mode 100755 index 0000000000..eb2aa0dbec --- /dev/null +++ b/tools/templates/configure.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# +# Copyright 2020 New Vector Ltd +# +# 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. +# + +echo "Configure RiotX Template..." +{ +ln -s $(pwd)/RiotXFeature /Applications/Android\ Studio.app/Contents/plugins/android/lib/templates/other +} && { + echo "Please restart Android Studio." +} diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt index bda4426c45..e82e8b3856 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt @@ -30,6 +30,10 @@ import io.reactivex.Single abstract class VectorViewModel<S : MvRxState, VA : VectorViewModelAction, VE : VectorViewEvents>(initialState: S) : BaseMvRxViewModel<S>(initialState, false) { + interface Factory<S: MvRxState> { + fun create(state: S): BaseMvRxViewModel<S> + } + // Used to post transient events to the View protected val _viewEvents = PublishDataSource<VE>() val viewEvents: DataSource<VE> = _viewEvents