javaandroidlibgdxsugarorm

How can I implement Sugar ORM in LibGDX


We are a group of french students. We are developing a game which requires a database. To simplify the code, we are using LibGdx. But, it seems like Sugar ORM is not bound with the application. We can't extends a SugarRecord.

I put the code of AndroidManifest.xml and the build.gradle (Module:Android). How we can fix this please?

EDIT: We create our classes in Android folder. Sugar Orm is not defined in the core.

<application
    android:name="com.orm.SugarApp"
    android:allowBackup="true"
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/GdxTheme">
    <meta-data
        android:name="DATABASE"
        android:value="AppName.db" />
    <meta-data
        android:name="VERSION"
        android:value="1" />
    <meta-data
        android:name="QUERY_LOG"
        android:value="true" />
    <meta-data
        android:name="DOMAIN_PACKAGE_NAME"
        android:value="com.AppName" />

The build.gradle (Module:Android):

dependencies {
    compile 'com.github.satyan:sugar:1.3'
    //other dependencies
}

Thanks you!


Solution

  • Bonjour! I'm going to make a few assumptions (correct me if they're wrong and we'll refine the answer below):

    1. The issue is that you're trying to extend SugarRecord<T> and your compiler/IDE can't see that class.
    2. The class you're trying to have extend SugarRecord<T> is located in core/src/..., not android/src/...

    What is the problem?

    When you write software (doesn't matter if it's games), you want to pay attention to how you divide your software into parts and how those parts interact. In particular, you want to be able to know when a change to one part will break another.

    libGDX separates its code into parts based on platform (that's why you have a core project and a desktop project and an android project). core is supposed to have all code that is platform independent (i.e. the stuff that is the same whether you're on a PC or a mobile device), and your other projects take care of platform specific stuff.

    Sugar ORM is an android-specific thing, so you (correctly) put it into your android gradle project dependencies. However, that means that only your code under the android/src folder knows about Sugar ORM and can use it. I'm pretty sure that's what's causing your issue. Problem is, the classes you want to save are almost certainly under core/src, and they belong there, so how do we fix it?

    The Easy Way to Fix

    If you only intend for your program to ever run on android (and you have a tight deadline for your project :wink: ), you can move the dependency from your Android gradle project to your Core gradle project. This will allow you to use those classes everywhere, but it will mean trouble when you try to build the Desktop/iOS/HTML projects.

    The Right Way to Fix

    If you want to fix it the right way (and maybe impress your professor with your mad coding abilities), you need to use something called Dependency Injection. Dependency injection is when we take something that our code needs, and we provide it to the code at runtime. This means we can decide on the fly whether we pass an android database or an iOS database. Like @Arctic45 said in the comments, the libGDX Wiki has a brief overview of this technique, but we'll go into a little more detail.

    For this example, I'm going to assume a simple game with a Monster class which looks something like this:

    // Lives in core
    public class Monster {
        public String name;     // prénom
        public int hitpoints;   // points de dommage
    
        public Monster() {
            this.name = "Tim the Unnamed";
            this.hitpoints = 1;
        }
        public Monster(String name, int hitpoints) {
            this.name = name;
            this.hitpoints = hitpoints;
        }
        @Override
        public String toString() {
            return String.format("{name: '%s', hitpoints: %n}", this.name, this.hitpoints);
        }
    
        public void attack(Monster other) {
            // Game specific logic...
        }
    }
    

    Now we want to be able to save this to a database, but we don't know if it's going to be an Android database or an iOS database or maybe even a database that's on the web somewhere (like Firebase). How do we handle that?

    What we do is we give core a DatabaseWrapper interface. This interface provides the methods we need, but doesn't include how they're implemented- it acts like a promise. core can plan on using these methods, and then we'll provide them later once we know which platform we're on. Here's an example application below which illustrates this technique:

    // Lives in core
    // Replace with your application
    public class LibGDXTestbed extends ApplicationAdapter {
        DatabaseWrapper database;
    
        public LibGDXTestbed() { } // For platforms that don't have databases to inject.
    
        public LibGDXTestbed(DatabaseWrapper database) {
            this.database = database;
        }
    
        /**
         * For demo purposes, add a new randomized monster to the database, then log a list of all the
         * monsters created to date.
         */
        @Override
        public void create () {
            if(database != null) {
                createMonster();
                printMonsters();
            } else {
                Gdx.app.error("WARNING", "No database provided. Load/Save Functionality Disabled.");
            }
        }
    
        // Helper method
        private void createMonster() {
            // Create a set of names we can use for new monsters.
            String[] names = {"Fred", "Mary", "Jean", "Tim"};
    
            String randomName = new Array<String>(names).random();
            int randomHP = MathUtils.random(100);
            database.saveMonster(new Monster(randomName, randomHP));
        }
    
        // Helper method
        private void printMonsters() {
            for(Monster monster : database.getMonsters()) {
                Gdx.app.log("DEBUG", monster.toString());
            }
        }
    }
    

    Note that the above doesn't know anything about Sugar ORM or make any assumptions about how the database works.

    The wrapper itself looks something like this:

    // Located in core
    public interface DatabaseWrapper {
        public void saveMonster(Monster monster);
        public List<Monster> getMonsters();
    }
    

    Now this is a little contrived (and could be refactored to be more generic), but it illustrates the point.

    Next, we create the android-specific code we need to implement this database. First we'll create a SugarMonster class which extends SugarRecord (since we don't want to do that with our core Monster class itself):

    // Lives in android/src
    public class SugarMonster extends SugarRecord<SugarMonster> {
        public String name;     // prénom
        public int hitpoints;   // points de dommage
    
        public SugarMonster() {
        }
    
        public SugarMonster(Monster monster) {
            this.name = monster.name;
            this.hitpoints = monster.hitpoints;
        }
    }
    

    We're also going to need a SugarWrapper class which implements our DatabaseWrapper class using Sugar ORM behind the scenes:

    // Lives in android/src
    public class SugarWrapper implements DatabaseWrapper {
        @Override
        public void saveMonster(Monster monster) {
            SugarMonster data = new SugarMonster(monster);
            data.save();
        }
    
        @Override
        public List<Monster> getMonsters() {
            List<SugarMonster> records = SugarMonster.listAll(SugarMonster.class);
            ArrayList<Monster> monsters = new ArrayList<>();
            for(SugarMonster record : records) {
                monsters.add(new Monster(record.name, record.hitpoints));
            }
            return monsters;
        }
    }
    

    Finally, we need to update our AndroidLauncher class to inject our database wrapper:

    // Lives in android/src
    public class AndroidLauncher extends AndroidApplication {
        @Override
        protected void onCreate (Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            AndroidApplicationConfiguration config = new AndroidApplicationConfiguration();
            initialize(new LibGDXTestbed(new SugarWrapper()), config);
        }
    }
    

    Bonus

    Another cool thing is that if you implement this the "right" way, it options up some cool possibilities for testing. If you want to write unit tests against your code, you can create a TestWrapper which implements DatabaseWrapper and mimics the database functionality with static data:

    public class TestWrapper implements DatabaseWrapper {
        List<Monster> monsters;
        public TestWrapper() {
            this.monsters = new ArrayList<>();
            this.monsters.add(new Monster("Tim the Tester", 123));
        }
        @Override
        public void saveMonster(Monster monster) {
            this.monsters.add(monster);
        }
    
        @Override
        public List<Monster> getMonsters() {
            return this.monsters;
        }
    }