Git Product home page Git Product logo

recompose's Introduction

recompose

recompose is a tool for converting Android layouts in XML to Kotlin code using Jetpack Compose. It can be used on the command line, as IntelliJ / Android Studio plugin or as a library in custom code.

Running

IntelliJ / Android Studio plugin

The plugin does not get published yet. If you want to try them then you'll have to build them from code. Use the Gradle task runIde to run a test IDE with the plugin installed or create an installable plugin with the buildPlugin task. The plugin zip will be placed in recompose-idea/build/distributions/. To learn how to install it, read the Install plugin from disk section in the IntelliJ docs.

Command-line interface (CLI)

Like the plugin, the command-line interface does not get published yet. You can run it directly from Gradle via ./gradlew recompose-cli:run --args="list file paths..". Alternatively you can run the assembleDist which will place a zip/tar containing a binary into recompose-cli/build/distributions/.

Usage: recompose [OPTIONS] INPUT...

Options:
  -o, --output PATH  Output directory for Kotlin code
  -h, --help         Show this message and exit

Arguments:
  INPUT  Layout XML files to convert to Kotlin

Building

Either import the project into IntelliJ IDEA or use Gradle on the command line (via the provided gradlew wrapper).

Modules

  • recompose-ast: Contains the data classes for the Abstract Syntax Tree (AST) representing a parsed XML layout.
  • recompose-cli: A command-line interface (CLI) for running recompose in a shell.
  • recompose-composer: Responsible for taking an AST and transforming it into the equivalent Composable Kotlin code.
  • recompose-idea: An IntelliJ IDEA / Android Studio plugin that allows copying XML layouts and pasting as Composable Kotlin code.
  • recompose-test: Contains test data and helpers for unit tests.

Important gradle tasks

  • clean: Deletes the build directories.
  • test: Runs all unit tests in all modules.
  • runIde: Runs IntelliJ IDEA with the plugin installed.
  • buildPlugin: Builds the IntelliJ / Android Studio plugin and makes it available in recompose-idea/build/distributions.

How does this work?

The Parser (recompose-parser) takes the input XML and transforms it into an Abstract Syntax Tree (recompose-ast). The Composer (recompose-composer) takes the AST and translates it into Kotlin code calling Composables.

The IntelliJ / Android Studio plugin (recompose-idea) uses that to perform the translation when pasting copied XML code. And the CLI (recompose-cli) uses it to translate files.

Can I contribute?

Yes, absolutely. There are a ton of Views and attributes to support. The list of issues labeled with good first issue are a good place to start. An issue from the list labeled with help wanted may be a good follow-up. Just comment on any issue that interests you.

License

Copyright 2020 Sebastian Kaspari

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.

recompose's People

Contributors

foso avatar jerryokafor avatar omarmiatello avatar pocmo avatar pt2121 avatar t-regbs avatar tieskedh 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

recompose's Issues

Edittext seems to close the parent tag

The parser seems to close a parent tag for EditText.

FrameLayout {
    EditText()
    View()
}

Will result in:

FrameLayout {
    EditText()
}
View()

Same occurs with LinearLayout.

Generate code and build against compose libraries?

alpha04 was just released and as expected some things have changed. Since that is likely to happen more often, it would be nice if we could compile the generated code for our test cases against the jetpack compose libraries. That would reveal breakage whenever we switch to the latest version.

Add CI setup

The usual. Run build and tests on PRs and pushes.

Wrap code inserting Kotlin code in runWriteAction()

In the plugin code we are replacing the selection with the transformed Kotlin code here:

editor.document.replaceString(
bounds.startOffset,
bounds.endOffset,
code

This yields a warning. IntelliJ wants us to wrap this in runWriteAction().
https://jetbrains.org/intellij/sdk/docs/basics/architectural_overview/general_threading_rules.html

Show a confirmation dialog before pasting

When using the Kotlin language plugin and pasting Java code then a popup will be shown, asking for confirmation first. This makes sense here too, to avoid pasting Kotlin code when this is not desired.

Screenshot 2020-09-15 at 09 28 35

TextView: Add support for android:maxLines

android:maxLines
Makes the TextView be at most this many lines tall. When used on an editable text, the inputType attribute's value must be combined with the textMultiLine flag for the maxLines attribute to apply.

XML attribute:
https://developer.android.com/reference/android/widget/TextView?hl=en#attr_android:maxLines

Jetpack Compose:

Text(
   ...
   maxLines = Int = Int.MAX_VALUE
   ...
)

We are parsing TextView here:
https://github.com/pocmo/recompose/blob/main/recompose-parser/src/main/kotlin/recompose/parser/xml/view/TextView.kt

And create a TextViewNode from it:
https://github.com/pocmo/recompose/blob/main/recompose-ast/src/main/kotlin/recompose/ast/view/TextViewNode.kt

And we turn the node into Kotlin code here:
https://github.com/pocmo/recompose/blob/main/recompose-composer/src/main/kotlin/recompose/composer/visitor/ComposingVisitor.kt#L82-L94

To solve this issue:

  • Parse the attribute and add the value to TextViewNode. Probably a nullable Int? is needed.
  • Add it to the list of parameters in ComposingVisitor.
  • Add a test case to ParserTest and ComposerTest.

recompose.parser.Parser$ParserException: Unknown drawable format: @color/white

Hi, tnx for plugin,
is it possible to fix this error:

recompose.parser.Parser$ParserException: Unknown size value: @dimen/common_margin
at recompose.parser.values.SizeKt.size(Size.kt:31)
at recompose.parser.values.PaddingKt.padding(Padding.kt:29)
at recompose.parser.xml.ViewKt.viewAttributes(View.kt:82)
at recompose.parser.xml.viewgroup.LinearLayoutKt.linearLayout(LinearLayout.kt:33)
at recompose.parser.xml.ViewKt.node(View.kt:51)
at recompose.parser.xml.ViewGroupKt.viewGroupAttributes(ViewGroup.kt:37)
at recompose.parser.xml.viewgroup.UnknownKt.unknown(Unknown.kt:31)
at recompose.parser.xml.ViewKt.node(View.kt:69)
at recompose.parser.xml.ViewGroupKt.viewGroupAttributes(ViewGroup.kt:37)
at recompose.parser.xml.viewgroup.ConstraintLayoutKt.constraintLayout(ConstraintLayout.kt:32)
at recompose.parser.xml.ViewKt.node(View.kt:65)
at recompose.parser.xml.ViewGroupKt.viewGroupAttributes(ViewGroup.kt:37)
at recompose.parser.xml.viewgroup.UnknownKt.unknown(Unknown.kt:31)
at recompose.parser.xml.ViewKt.node(View.kt:69)
at recompose.parser.xml.LayoutKt.layout(Layout.kt:37)
at recompose.parser.Parser.parse(Parser.kt:62)
at recompose.parser.Parser.parse(Parser.kt:53)
at recompose.parser.Parser.parse(Parser.kt:37)

Check paste destination

When pasting translated Kotlin code with the plugin we should check whether the destination is valid (Is it a Kotlin file?).

Right now if you copy XML and paste it back into an XML then we paste Kotlin code back into the XML file. :)

Spike: Writing a "Composer" that has access to the plugin context

Currently the Composer (code) is an independent class that writes Kotlin code and outputs it as a string. This string is then used by the plugin and CLI.

What if we'd implement a Composer that knows its inside an IntelliJ / Android Studio plugin and can make use of the environment and tools to write the code to the destination. A big advantage would be that we'd get access to the AST of the surrounding code and could, for example, figure out if a drawable is an image or a vector (#69). Maybe we would also be able to use the internals for generating/writing code and wouldn't need to write a plain string?

Considering the composer is the "backend" and the parser is the "frontend" we may also be able to replace the "frontend" and let IntelliJ parse the XML and give us access to its AST?

A downside of this would be that this would basically reduce this project to the plugin and potentially kill the CLI. But then again, maybe parts of it could also survive in a standalone version. After all projects like detekt also make use of the parts of the Kotlin compiler without being a plugin.

Having a closer look at the Java to Kotlin conversion in the Kotlin plugin may give some hints about what is possible.

Image: imageResource vs. vectorResource

Currently a drawable value in ImageView will always be written as:

Image(imageResource(R.id.the_drawable), ...)

This works for drawable resources that are PNGs. But it fails if the drawable is a vector image. In that case it needs to be:

Image(vectorResource(R.id.the_drawable), ...)

From the XML alone we cannot know if the drawable is a vector resource or not. We could try finding the drawable (assuming default folders are used), but this fails if the resource is not in the app itself (e.g. provided by a library or a different module).

Alternatively the plugin in the IDE could try to figure out what kind of drawable is used - but this will only work in the plugin and not in the CI.

In general I am surprised to see Jetpack Compose force making this distinction unto the consumer. Wouldn't it be more convenient to provide a drawable resource to Compose and it can figure out itself what it is?

Add support for app:layout_constraintHorizontal_chainStyle/layout_constraintVertical_chainStyle

  • Find chains in children: "A set of widgets are considered a chain if they are linked together via a bi-directional connection"
  • Identify chain heads: "The head is the left-most widget for horizontal chains, and the top-most widget for vertical chains."
  • Use layout_constraintHorizontal_chainStyle/layout_constraintVertical_chainStyle of head

Composable example:

ConstraintLayout {
    createHorizontalChain(eye_left, eye_right, chainStyle = ChainStyle.Packed)

    ...
}

Formatting: When to use line breaks?

Currently we do not use line breaks for formatting.

So for example we'd create the following Text Composable:

Text(text = "I am a test", color = Color(0xffffcc00.toInt()), fontSize = 20.sp, modifier = Modifier.width(100.dp).background(Color(0xaa0000ff.toInt())))

We probably should define a threshold of number of arguments/modifier, where we start using line breaks, e.g.:

Text(
    text = "I am a test",
    color = Color(0xffffcc00.toInt()),
    fontSize = 20.sp,
    modifier = Modifier
        .width(100.dp)
        .background(Color(0xaa0000ff.toInt()))
)

KotlinWriterHelper

I saw you were looking for a higher level for KotlinWriter?
Maybe something like this is an option?

The code below

internal class KotlinWriterHelper(
        val writer: KotlinWriter
) {
    operator fun String.invoke(
            vararg parameters: CallParameter?,
            block: (KotlinWriter.() -> Unit)? = null
    ) = writer.writeCall(
            name = this,
            parameters = parameters.toList(),
            block = block
    )
    fun write(writer: KotlinWriterHelper.() -> Unit) = writer()
}

would change this code

writer.writeCall(
       "Row",
       parameters = listOf(
           rowModifier.toCallParameter()
        )
) {
    writeCall(
        name = "RadioButton",
        parameters = listOf(
            CallParameter(
                name = "selected",
                value = ParameterValue.RawValue(node.checked)
            ),
            CallParameter(
                name = "onCheckedChange",
                value = ParameterValue.EmptyLambdaValue
            )
       )
   )
}

into

write{
    "Row"(
         rowModifier.toCallParameter()
    ){
    "RadioButton"(
        CallParameter(
             name = "selected",
             value = ParameterValue.RawValue(node.checked)
        ),
        CallParameter(
            name = "onCheckedChange",
            value = ParameterValue.EmptyLambdaValue
        )
    )
}

Adding functions like:

    internal infix fun String.withSizeValue(size: Size) = CallParameter(ParameterValue.SizeValue(size), this)
    internal infix fun String.withColorValue(color: Color) = CallParameter(ParameterValue.ColorValue(color), this)
    internal infix fun String?.withRawValue(raw: String) = CallParameter(ParameterValue.RawValue(raw), this)
    internal infix fun String.withRawValue(raw: Boolean) = CallParameter(ParameterValue.RawValue(raw), this)
    internal infix fun String.withRawValue(raw: Int) = CallParameter(ParameterValue.RawValue(raw), this)
    internal infix fun String?.withStringValue(raw: String) = CallParameter(ParameterValue.StringValue(raw), this)
    internal fun String.withEmptyLambda() = CallParameter(ParameterValue.EmptyLambdaValue, this)
    internal infix fun String.withKeyboardType(inputType: InputType) = CallParameter(ParameterValue.KeyboardTypeValue(inputType), this)

would simplify it to:

write{
    "Row"(
         rowModifier.toCallParameter()
    ){
    "RadioButton"(
        "selected" withRawValue node.checked,
        "onCheckedChange".withEmptyLambda()
    )
}

(these params could be written in an interal interface, which then would be implemented by the KotlinWriterHelper)

We then could make the names values or getters inside an interface:

    val Text = "Text"
    val Box = "Box"
    val Button = "Button"
    val Row = "Row"
    val Checkbox = "Checkbox"
    val RadioButton = "RadioButton"
    val Card = "Card"
    val Image = "Image"
    val TextField = "TextField"

Such that we can write:

write{
    Row(
         rowModifier.toCallParameter()
    ){
    RadioButton(
        "selected" withRawValue node.checked,
        "onCheckedChange".withEmptyLambda()
    )
}

And we could add a Text function:

fun KotlinWriterHelper.Text(
        vararg params : CallParameter?,
        text: String? = null,
    ) = "Text"(
        text?.let {
            null withStringValue it
        },
        *params
    )
)

(I placed the varargs at the start, as that would keep compiling all the old code)

Add support for MaterialCardView

com.google.android.material.card.MaterialCardView extends androidx.cardview.widget.CardView and could use the same parser.

View available in:

com.google.android.material:material:1.2.1

Support ImageView

https://developer.android.com/reference/android/widget/ImageView

 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
     <ImageView
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:src="@drawable/my_image"
         android:contentDescription="@string/my_image_description"
         />
 </LinearLayout>

Map 0dp width/height in ConstraintLayout to fillMaxWidth()/fillMaxHeight()

Using ConstraintLayout you'd often set android:layout_width or android:layout_height of a View to 0dp to let the ConstraintLayout stretch it based on the constraints. ConstraintLayout() doesn't behave that way if you add a width(0.dp) modifier. In that case we need to translate it to fillMaxWidth() or fillMaxHeight().

For now we could just always translate 0dp to fillMaxWidth() or fillMaxHeight() if the node has any constraint set.

On paste: Add required imports

Currently when pasting with the plugin we only add the code. But we can also add the required imports to the list of import statements (e.g. import androidx.compose.foundation.Text).

From reading the code of the Kotlin language plugin, I was expecting something like this to work:

val import = KtPsiFactory(project, true).createImportDirective(
    ImportPath(FqName("androidx.compose.foundation.Text"), false)
)

val target = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) as KtFile
target.importList!!.imports.add(import)

But I end up with the following error:

e: recompose/recompose-idea/src/main/kotlin/recompose/plugin/copypaste/RecomposeCopyPasteProcessor.kt: (101, 16): Cannot access 'com.intellij.psi.PsiClassOwner' which is a supertype of 'org.jetbrains.kotlin.psi.KtFile'. Check your module classpath for missing or conflicting dependencies
e: recompose/recompose-idea/src/main/kotlin/recompose/plugin/copypaste/RecomposeCopyPasteProcessor.kt: (101, 16): Cannot access 'com.intellij.psi.PsiModifiableCodeBlock' which is a supertype of 'org.jetbrains.kotlin.psi.KtFile'. Check your module classpath for missing or conflicting dependencies

I am not sure how to get access to those super types?

Handle unknown elements more gracefully

Currently we just bail if we hit an XML element that we do not recognize:
https://github.com/pocmo/recompose/blob/main/recompose-parser/src/main/kotlin/recompose/parser/xml/View.kt#L48

This is of course pretty annoying for someone using the plugin. Ideally we'd parse it as an UnknownNode and transform this in the Composer into a comment that will make the user aware that there's a piece missing here. With that the user still gets the benefit of translating everything else and can then fill in the blanks.

Support incomplete XML input

Needed for #5.

The XMLPullParser implementation only likes well-formed XML documents. E.g. when having multiple root nodes:

start tag not allowed in epilog but got T (position: END_TAG seen ...    android:text="padding"\n    android:background="#ff0000" />\n\n<T... @9:3) 
org.xmlpull.v1.XmlPullParserException: start tag not allowed in epilog but got T (position: END_TAG seen ...    android:text="padding"\n    android:background="#ff0000" />\n\n<T... @9:3)

But we also want to be able to recompose partial XML documents (e.g. see #5).

We could:

  • Figure out if XMLPullParser supports a quirks mode that will continue parsing documents that are not strictly following the XML standard.
  • Manually fix up the XML before parsing it (e.g. introducing a root node that we will skip when parsing).
  • Use a different, more forgiving parser.
  • Write a minimal parser implementation that parses tags / attributes and doesn't require strict XML documents.

Add support for FrameLayout gravity

#11 added basic support for FrameLayout. This issue is about adding support for gravity so that children can be positioned in the FrameLayout / Box.

Also see discussion about how to map between the different gravity representations here: #11 (comment) about

Add support for Checkbox

Add support for padding

fun Modifier.padding(
    start: Dp = 0.dp,
    top: Dp = 0.dp,
    end: Dp = 0.dp,
    bottom: Dp = 0.dp
)

Only recompose selection, honoring startOffsets/endOffsets

Currently when copying layout XML with the plugin and pasting it into code, we will translate the whole file and paste the whole translation. That's of course not right and we need to look at the actual selection using startOffsets and endOffsets.

Of course using the selection means we may get incomplete XML or multiple "root" nodes. I am not sure what XMLPullParser will do with that. We may need to "fix up" the XML before processing it. Or manually reduce the selection to a valid XML subset.

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.