dkunzler / esperandro

Easy SharedPreference Engine foR ANDROid
Other
180 stars 14 forks source link

XML Preference Screen support #40

Closed MFlisar closed 7 years ago

MFlisar commented 8 years ago

Is it possible to use esperandro type safe with PreferenceScreens written in xml?

A simple solution for me would be that you just generate string resources for each preference key... So that I can reference them in the xml

Any ideas to that?

dkunzler commented 8 years ago

Interesting idea. But I have to look more into detail for this. I think there is a problem with the build order. Additionally I don't like the compilation being dependent of something generated. (the interfaces for esperandro are always there at compile time, so you will never import something that is not there yet; but if I generate something at build time that is needed at build time I could run into errors)

But I like the general idea of having more stuff tied to esperandro.

MFlisar commented 8 years ago

Actually depending on compilation results is common, even view binging works that way (only after compiling you can access the class)

I just thought about generating the resources because this probably is the easiest solution and can be done fastest...

I'm currently writing my setting screens for an app and have about 100 preferences I think... Having so many preferences, I would say the string resource solution would already fulfill all my needs... Auto complete + typo safe + if I rename something the compilation will fail the next time...

MFlisar commented 8 years ago

I just wrote all my variables to the console and then copied them into a preference.xml file... Code looks like following (maybe interesting for someone else in the meantime until you thought about if and how you implement that).

This is just a fast solution though...

public class Temp
{
    private static final String PREF_RW = "\t<string name=\"%s\" translatable=\"false\">%s</string>\n";

    // preferenceClass should be the preference interface
    public static void printPreferenceNamesAsStringResources(Class<?> preferenceClass)
    {
        Method[] methods = preferenceClass.getDeclaredMethods();

        StringBuilder sb = new StringBuilder();
        sb.append("<resources>");
        for (int i = 0; i < methods.length; i++)
        {
            Method m = methods[i];
            if (m.getParameterTypes().length == 0)
                addPref(sb, m);
        }
        sb.append("</resources>");
        log("PreferenceStrings", sb);
    }

    private static void addPref(StringBuilder sb, Method m)
    {
        sb.append(String.format(PREF_RW, m.getName(), m.getName()));
    }

    private static void log(String tag, StringBuilder sb)
    {
        if (sb.length() > 4000)
        {
            int parts = sb.length() / 4000;
            for (int i = 0; i <= parts; i++)
            {
                int max = 4000 * (i + 1);
                if (max >= sb.length())
                    Log.v(tag, sb.substring(4000 * i));
                else
                    Log.v(tag, sb.substring(4000 * i, max));
            }
        }
        else
            Log.v(tag, sb.toString());
    }
}
dkunzler commented 8 years ago

I think I could generate String resources like you said if I write a gradle plugin. I have to look into it a bit more. With the current solution I could possibly generate them into the wrong output folder ;) Will think about this a bit more. A bit more input is welcome. @danielneu

danielneu commented 8 years ago

I have to admit that I had no hands on Android since what feels like years, with no IDE installed anymore. I like the idea as well, but I am most likely not able to contribute anything else.

Sorry :-/

MFlisar commented 8 years ago

@dkunzler anything special you need to know because you are asking for more input?

Actually, the solution should be quite simple, but I've no experience with annotation processors. I checked that a little bit already but I don't even know how to create a file in a special destination.

The destination for the output should be a seperate xml file in res\values, a file like preferences.xml for a class named Preferences.java for example, maybe even with a esperandro_, esp_ or similar prefix? To avoid conflicts...

dkunzler commented 8 years ago

@MFlisar I hacked something together that should work well. If you don't mind you could beta test the feature and give feedback.

To activate for you:

add my maven repo to top buid.gradle (or wherever you like it):

allprojects {
    repositories {
        jcenter()
        maven {
            url "http://maven.devland.de"
        }
    }
}

add snapshot dependencies:

compile 'de.devland.esperandro:esperandro-api:2.3.2-SNAPSHOT'
apt 'de.devland.esperandro:esperandro:2.3.2-SNAPSHOT'

Add this to your apps build.gradle to tell esperandro where your resources are:

apt {
    arguments {
        esperandro_resDir android.sourceSets.main.res.srcDirs[0]
    }
}

Add the @GenerateStringResources Annotation to your preference interface. You can customize the stringPrefix which means a prefix that is added to the defined string instead of only using the name. Additionally you can set filePrefix to add a prefix to the generated xml file. The files are generated directly into your project. It was the only way I found without writing a custom gradle plugin. (which would mean to migrate this whole project to gradle I think)

Every feedback is welcome.

MFlisar commented 8 years ago

I'll test that... You did not change anything else, did you? Only code that generates the xml string resource file was added...

I've two apps I will test this with today...

MFlisar commented 8 years ago

My observations so far:

MFlisar commented 8 years ago

Additional observations:

When I try to make a release build I get following exception:

Error:Error converting bytecode to dex:
Cause: Dex cannot parse version 52 byte code.
This is caused by library dependencies that have been compiled using Java 8 or above.
If you are using the 'java' gradle plugin in a library submodule add 
targetCompatibility = '1.7'
sourceCompatibility = '1.7'
to that submodule's build.gradle file.

Adding the `compileOptions` to my project does not help...
dkunzler commented 8 years ago

I uploaded a new SNAPSHOT to my maven repo.

Changes:

BTW: I'm also thinking about generating another class that can be accessed directly which contains the names of the preferences as string constants to be used in code interacting with the preferences directly. What do you think of that?

MFlisar commented 8 years ago

Looks good now. Thanks

I don't mind if you add this, but this only increases speed a little bit in some cases. context.getString(...) can be used in code as well with the generated string resources...

MFlisar commented 7 years ago

Can you tell me how to convert this statement for the new annotation processor?

apt {
    arguments {
        esperandro_resDir android.sourceSets.main.res.srcDirs[0]
    }
}

Tried following:

javaCompileOptions {
            annotationProcessorOptions {
                arguments = [
                        squidbOptions : 'androidModels',
                        esperandro_resDir : android.sourceSets.main.res.srcDirs[0]
                ]
            }
        }

Tried esperandro_resDir : 'src/main/res', but this does not work either...

Edit

esperandro_resDir : 'C:/my/absolute/path/src/main/res' works, do you know how to get the absolute path?

dkunzler commented 7 years ago

I will look into this. What versions are you using currently? AS, Gradle, android-gradle-plugin, apt-plugin. And a redacted version of your build.gradle would also be nice. Thanks

MFlisar commented 7 years ago

I use version 2.4.1, gradle 4.0, latest stable android studio, NO APT PLUGIN but the newer annotationProcessor

Can only add the gradle file later...

MFlisar commented 7 years ago

That's what I use in the build.gradle:

compile 'de.devland.esperandro:esperandro-api:2.4.1'
annotationProcessor 'de.devland.esperandro:esperandro:2.4.1'
dkunzler commented 7 years ago

Thanks, I will definately setup a test project and look into it later. Didn't have the time yesterday, sorry.

MFlisar commented 7 years ago

No need to hurry, a hard coded path does work anyways... Still, not useable for an open source project or a shared project...

Two solutions are possible:

  1. add the project path in the buid.gradle, not sure how to retrieve it though. Something like root + '/src/main/res'...
  2. support absolute and relative paths and append the project path in the processor if necessary - here: https://github.com/dkunzler/esperandro/blob/master/processor/src/main/java/de/devland/esperandro/processor/EsperandroAnnotationProcessor.java#L398 Not sure if can retrieve the project root path in the processor?
dkunzler commented 7 years ago

Hi,

the following inside android { defaultConfig {...} } works for me locally.

    javaCompileOptions {
        annotationProcessorOptions {
            arguments = [
                    esperandro_resDir : android.sourceSets.main.res.srcDirs[0].getAbsolutePath()
            ]
        }
    }

Reach back if it doesn't work. Unfortunately I don't see any possibility to get the desired res dir right from the processor.

MFlisar commented 7 years ago

Auto complete does not show me getAbsolutePath() that's why I did not try this...

Works now, thanks.

But it generates the strnigs in the main/res/values folder instead of the main/res/values/strings

dkunzler commented 7 years ago

Yeah, because that is the path where strings are going. If you put them in values/strings you could add + "/strings" to the path. I'm still thinking about a better solution but the annotation processor had not enough information to find the res folder on its own. Perhaps the new integrated one gives more information. Would love to make this feature publicly available wihtout the need of configuring something in the build.gradle.

MFlisar commented 7 years ago

I know. You should put this into your readme. As far as I know it's the one thing that's unique to your library, others don't have that. Just like the caching feature.

Supporting generation of strings without setup would be nice. Maybe you can traverse the folder hierarchy until you find the root with the gradle file and then search for the res folder... such an idea. Path of the annotated class is known as far as I remember from my annotation library.

Issue is solved, thanks for your help.

dkunzler commented 7 years ago

@MFlisar FYI I just released 2.5.0 (should be available on central in a few hours) Features that are potentially interesting for you:

I will update the documentation for all that later that week.

MFlisar commented 7 years ago

Tried the list functions already, very useful.

Ever thought about the possibility to create a preference screen from the preference setup?