Merge pull request #1304 from vector-im/feature/template

Feature/template
This commit is contained in:
ganfra 2020-04-29 19:21:01 +02:00 committed by GitHub
commit 21d0db8382
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 367 additions and 0 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,5 @@
package ${escapeKotlinIdentifiers(packageName)}
import im.vector.riotx.core.platform.VectorViewModelAction
sealed class ${actionClass}: VectorViewModelAction

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,5 @@
package ${escapeKotlinIdentifiers(packageName)}
import im.vector.riotx.core.platform.VectorViewEvents
sealed class ${viewEventsClass} : VectorViewEvents

View File

@ -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
}
}

View File

@ -0,0 +1,5 @@
package ${escapeKotlinIdentifiers(packageName)}
import com.airbnb.mvrx.MvRxState
data class ${viewStateClass}() : MvRxState

View File

@ -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>

24
tools/templates/configure.sh Executable file
View File

@ -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."
}

View File

@ -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