Frage Senden Sie eine Anwendung mit einer Datenbank


Wenn Ihre Anwendung eine Datenbank benötigt und sie über integrierte Daten verfügt, wie kann diese Anwendung am besten geliefert werden? Sollte ich:

  1. Erstellen Sie die SQLite-Datenbank vor und fügen Sie sie in das Verzeichnis ein .apk?

  2. Fügen Sie die SQL-Befehle in die Anwendung ein und lassen Sie die Datenbank erstellen und die Daten bei der ersten Verwendung einfügen?

Die Nachteile, die ich sehe, sind:

  1. Mögliche SQLite-Versionskonflikte können Probleme verursachen und ich weiß derzeit nicht, wo die Datenbank hin soll und wie ich darauf zugreifen kann.

  2. Es kann sehr lange dauern, die Datenbank auf dem Gerät zu erstellen und aufzufüllen.

Irgendwelche Vorschläge? Hinweise auf die Dokumentation bezüglich irgendwelcher Probleme würden sehr geschätzt werden.


882
2018-02-04 20:11


Ursprung


Antworten:


Ich habe gerade einen Weg gefunden, dies in ReignDesign Blog in einem Artikel mit dem Titel zu tun Verwenden Sie Ihre eigene SQLite-Datenbank in Android-Anwendungen. Grundsätzlich erstellen Sie Ihre Datenbank vorab, legen Sie sie in Ihrem Anlagenverzeichnis in Ihrer apk ab und kopieren Sie sie bei der ersten Verwendung in das Verzeichnis "/ data / data / YOUR_PACKAGE / databases /".


441
2018-03-06 19:23



Es gibt zwei Möglichkeiten, Datenbanken zu erstellen und zu aktualisieren. 

Eine besteht darin, eine Datenbank extern zu erstellen, sie dann in den Assets-Ordner des Projekts zu platzieren und dann die gesamte Datenbank von dort zu kopieren. Dies ist viel schneller, wenn die Datenbank viele Tabellen und andere Komponenten enthält. Upgrades werden ausgelöst, indem die Versionsnummer der Datenbank in der Datei res / values ​​/ strings.xml geändert wird. Upgrades werden dann durchgeführt, indem eine neue Datenbank extern erstellt wird, die alte Datenbank im Anlagenordner durch die neue Datenbank ersetzt wird, die alte Datenbank im internen Speicher unter einem anderen Namen gespeichert wird, die neue Datenbank vom Anlagenordner in den internen Speicher kopiert wird und alle Daten übertragen werden der Daten aus der alten Datenbank (die früher umbenannt wurde) in die neue Datenbank und schließlich das Löschen der alten Datenbank. Sie können eine Datenbank ursprünglich mithilfe von erstellen SQLite Manager FireFox-Plugin um Ihre SQL-Anweisungen zur Erstellung auszuführen.

Die andere Möglichkeit besteht darin, eine Datenbank intern aus einer SQL-Datei zu erstellen. Dies ist nicht so schnell, aber die Verzögerung wäre wahrscheinlich für die Benutzer nicht wahrnehmbar, wenn die Datenbank nur wenige Tabellen hat. Upgrades werden ausgelöst, indem die Versionsnummer der Datenbank in der Datei res / values ​​/ strings.xml geändert wird.  Upgrades werden dann durch die Verarbeitung einer Upgrade-SQL-Datei durchgeführt. Die Daten in der Datenbank bleiben unverändert, außer wenn der Container entfernt wird, z. B. wenn eine Tabelle gelöscht wird.

Im folgenden Beispiel wird veranschaulicht, wie eine der Methoden verwendet wird.

Hier ist ein Beispiel create_database.sql Datei. Sie wird für die interne Methode in den Ordner "Assets" des Projekts gestellt oder in "SQL ausführen" von SQLite Manager kopiert, um die Datenbank für die externe Methode zu erstellen. (Hinweis: Beachten Sie den Kommentar zu der von Android benötigten Tabelle.)

--Android requires a table named 'android_metadata' with a 'locale' column
CREATE TABLE "android_metadata" ("locale" TEXT DEFAULT 'en_US');
INSERT INTO "android_metadata" VALUES ('en_US');

CREATE TABLE "kitchen_table";
CREATE TABLE "coffee_table";
CREATE TABLE "pool_table";
CREATE TABLE "dining_room_table";
CREATE TABLE "card_table"; 

Hier ist eine Beispieldatei update_database.sql. Sie wird für die interne Methode in den Ordner "Assets" des Projekts gestellt oder in "SQL ausführen" von SQLite Manager kopiert, um die Datenbank für die externe Methode zu erstellen. (HINWEIS: Beachten Sie, dass alle drei Arten von SQL-Kommentaren vom SQL-Parser ignoriert werden, der in diesem Beispiel enthalten ist.)

--CREATE TABLE "kitchen_table";  This is one type of comment in sql.  It is ignored by parseSql.
/*
 * CREATE TABLE "coffee_table"; This is a second type of comment in sql.  It is ignored by parseSql.
 */
{
CREATE TABLE "pool_table";  This is a third type of comment in sql.  It is ignored by parseSql.
}
/* CREATE TABLE "dining_room_table"; This is a second type of comment in sql.  It is ignored by parseSql. */
{ CREATE TABLE "card_table"; This is a third type of comment in sql.  It is ignored by parseSql. }

--DROP TABLE "picnic_table"; Uncomment this if picnic table was previously created and now is being replaced.
CREATE TABLE "picnic_table" ("plates" TEXT);
INSERT INTO "picnic_table" VALUES ('paper');

Hier ist ein Eintrag, der der Datei /res/values/strings.xml für die Datenbankversionsnummer hinzugefügt werden muss.

<item type="string" name="databaseVersion" format="integer">1</item>

Hier ist eine Aktivität, die auf die Datenbank zugreift und sie dann verwendet. (Hinweis: Möglicherweise möchten Sie den Datenbankcode in einem separaten Thread ausführen, wenn er viele Ressourcen verwendet.)

package android.example;

import android.app.Activity;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;

/**
 * @author Danny Remington - MacroSolve
 * 
 *         Activity for demonstrating how to use a sqlite database.
 */
public class Database extends Activity {
     /** Called when the activity is first created. */
     @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        DatabaseHelper myDbHelper;
        SQLiteDatabase myDb = null;

        myDbHelper = new DatabaseHelper(this);
        /*
         * Database must be initialized before it can be used. This will ensure
         * that the database exists and is the current version.
         */
         myDbHelper.initializeDataBase();

         try {
            // A reference to the database can be obtained after initialization.
            myDb = myDbHelper.getWritableDatabase();
            /*
             * Place code to use database here.
             */
         } catch (Exception ex) {
            ex.printStackTrace();
         } finally {
            try {
                myDbHelper.close();
            } catch (Exception ex) {
                ex.printStackTrace();
            } finally {
                myDb.close();
            }
        }

    }
}

Hier ist die Datenbank-Hilfsklasse, in der die Datenbank erstellt oder bei Bedarf aktualisiert wird. (Hinweis: Android erfordert, dass Sie eine Klasse erstellen, die SQLiteOpenHelper erweitert, um mit einer SQLite-Datenbank zu arbeiten.)

package android.example;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

/**
 * @author Danny Remington - MacroSolve
 * 
 *         Helper class for sqlite database.
 */
public class DatabaseHelper extends SQLiteOpenHelper {

    /*
     * The Android's default system path of the application database in internal
     * storage. The package of the application is part of the path of the
     * directory.
     */
    private static String DB_DIR = "/data/data/android.example/databases/";
    private static String DB_NAME = "database.sqlite";
    private static String DB_PATH = DB_DIR + DB_NAME;
    private static String OLD_DB_PATH = DB_DIR + "old_" + DB_NAME;

    private final Context myContext;

    private boolean createDatabase = false;
    private boolean upgradeDatabase = false;

    /**
     * Constructor Takes and keeps a reference of the passed context in order to
     * access to the application assets and resources.
     * 
     * @param context
     */
    public DatabaseHelper(Context context) {
        super(context, DB_NAME, null, context.getResources().getInteger(
                R.string.databaseVersion));
        myContext = context;
        // Get the path of the database that is based on the context.
        DB_PATH = myContext.getDatabasePath(DB_NAME).getAbsolutePath();
    }

    /**
     * Upgrade the database in internal storage if it exists but is not current. 
     * Create a new empty database in internal storage if it does not exist.
     */
    public void initializeDataBase() {
        /*
         * Creates or updates the database in internal storage if it is needed
         * before opening the database. In all cases opening the database copies
         * the database in internal storage to the cache.
         */
        getWritableDatabase();

        if (createDatabase) {
            /*
             * If the database is created by the copy method, then the creation
             * code needs to go here. This method consists of copying the new
             * database from assets into internal storage and then caching it.
             */
            try {
                /*
                 * Write over the empty data that was created in internal
                 * storage with the one in assets and then cache it.
                 */
                copyDataBase();
            } catch (IOException e) {
                throw new Error("Error copying database");
            }
        } else if (upgradeDatabase) {
            /*
             * If the database is upgraded by the copy and reload method, then
             * the upgrade code needs to go here. This method consists of
             * renaming the old database in internal storage, create an empty
             * new database in internal storage, copying the database from
             * assets to the new database in internal storage, caching the new
             * database from internal storage, loading the data from the old
             * database into the new database in the cache and then deleting the
             * old database from internal storage.
             */
            try {
                FileHelper.copyFile(DB_PATH, OLD_DB_PATH);
                copyDataBase();
                SQLiteDatabase old_db = SQLiteDatabase.openDatabase(OLD_DB_PATH, null, SQLiteDatabase.OPEN_READWRITE);
                SQLiteDatabase new_db = SQLiteDatabase.openDatabase(DB_PATH,null, SQLiteDatabase.OPEN_READWRITE);
                /*
                 * Add code to load data into the new database from the old
                 * database and then delete the old database from internal
                 * storage after all data has been transferred.
                 */
            } catch (IOException e) {
                throw new Error("Error copying database");
            }
        }

    }

    /**
     * Copies your database from your local assets-folder to the just created
     * empty database in the system folder, from where it can be accessed and
     * handled. This is done by transfering bytestream.
     * */
    private void copyDataBase() throws IOException {
        /*
         * Close SQLiteOpenHelper so it will commit the created empty database
         * to internal storage.
         */
        close();

        /*
         * Open the database in the assets folder as the input stream.
         */
        InputStream myInput = myContext.getAssets().open(DB_NAME);

        /*
         * Open the empty db in interal storage as the output stream.
         */
        OutputStream myOutput = new FileOutputStream(DB_PATH);

        /*
         * Copy over the empty db in internal storage with the database in the
         * assets folder.
         */
        FileHelper.copyFile(myInput, myOutput);

        /*
         * Access the copied database so SQLiteHelper will cache it and mark it
         * as created.
         */
        getWritableDatabase().close();
    }

    /*
     * This is where the creation of tables and the initial population of the
     * tables should happen, if a database is being created from scratch instead
     * of being copied from the application package assets. Copying a database
     * from the application package assets to internal storage inside this
     * method will result in a corrupted database.
     * <P>
     * NOTE: This method is normally only called when a database has not already
     * been created. When the database has been copied, then this method is
     * called the first time a reference to the database is retrieved after the
     * database is copied since the database last cached by SQLiteOpenHelper is
     * different than the database in internal storage.
     */
    @Override
    public void onCreate(SQLiteDatabase db) {
        /*
         * Signal that a new database needs to be copied. The copy process must
         * be performed after the database in the cache has been closed causing
         * it to be committed to internal storage. Otherwise the database in
         * internal storage will not have the same creation timestamp as the one
         * in the cache causing the database in internal storage to be marked as
         * corrupted.
         */
        createDatabase = true;

        /*
         * This will create by reading a sql file and executing the commands in
         * it.
         */
            // try {
            // InputStream is = myContext.getResources().getAssets().open(
            // "create_database.sql");
            //
            // String[] statements = FileHelper.parseSqlFile(is);
            //
            // for (String statement : statements) {
            // db.execSQL(statement);
            // }
            // } catch (Exception ex) {
            // ex.printStackTrace();
            // }
    }

    /**
     * Called only if version number was changed and the database has already
     * been created. Copying a database from the application package assets to
     * the internal data system inside this method will result in a corrupted
     * database in the internal data system.
     */
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        /*
         * Signal that the database needs to be upgraded for the copy method of
         * creation. The copy process must be performed after the database has
         * been opened or the database will be corrupted.
         */
        upgradeDatabase = true;

        /*
         * Code to update the database via execution of sql statements goes
         * here.
         */

        /*
         * This will upgrade by reading a sql file and executing the commands in
         * it.
         */
        // try {
        // InputStream is = myContext.getResources().getAssets().open(
        // "upgrade_database.sql");
        //
        // String[] statements = FileHelper.parseSqlFile(is);
        //
        // for (String statement : statements) {
        // db.execSQL(statement);
        // }
        // } catch (Exception ex) {
        // ex.printStackTrace();
        // }
    }

    /**
     * Called everytime the database is opened by getReadableDatabase or
     * getWritableDatabase. This is called after onCreate or onUpgrade is
     * called.
     */
    @Override
    public void onOpen(SQLiteDatabase db) {
        super.onOpen(db);
    }

    /*
     * Add your public helper methods to access and get content from the
     * database. You could return cursors by doing
     * "return myDataBase.query(....)" so it'd be easy to you to create adapters
     * for your views.
     */

}

Hier ist die FileHelper-Klasse, die Methoden zum Byte-Stream-Kopieren von Dateien und zum Analysieren von SQL-Dateien enthält.

package android.example;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.nio.channels.FileChannel;

/**
 * @author Danny Remington - MacroSolve
 * 
 *         Helper class for common tasks using files.
 * 
 */
public class FileHelper {
    /**
     * Creates the specified <i><b>toFile</b></i> that is a byte for byte a copy
     * of <i><b>fromFile</b></i>. If <i><b>toFile</b></i> already existed, then
     * it will be replaced with a copy of <i><b>fromFile</b></i>. The name and
     * path of <i><b>toFile</b></i> will be that of <i><b>toFile</b></i>. Both
     * <i><b>fromFile</b></i> and <i><b>toFile</b></i> will be closed by this
     * operation.
     * 
     * @param fromFile
     *            - InputStream for the file to copy from.
     * @param toFile
     *            - InputStream for the file to copy to.
     */
    public static void copyFile(InputStream fromFile, OutputStream toFile) throws IOException {
        // transfer bytes from the inputfile to the outputfile
        byte[] buffer = new byte[1024];
        int length;

        try {
            while ((length = fromFile.read(buffer)) > 0) {
                toFile.write(buffer, 0, length);
            }
        }
        // Close the streams
        finally {
            try {
                if (toFile != null) {
                    try {
                        toFile.flush();
                    } finally {
                        toFile.close();
                    }
            }
            } finally {
                if (fromFile != null) {
                    fromFile.close();
                }
            }
        }
    }

    /**
     * Creates the specified <i><b>toFile</b></i> that is a byte for byte a copy
     * of <i><b>fromFile</b></i>. If <i><b>toFile</b></i> already existed, then
     * it will be replaced with a copy of <i><b>fromFile</b></i>. The name and
     * path of <i><b>toFile</b></i> will be that of <i><b>toFile</b></i>. Both
     * <i><b>fromFile</b></i> and <i><b>toFile</b></i> will be closed by this
     * operation.
     * 
     * @param fromFile
     *            - String specifying the path of the file to copy from.
     * @param toFile
     *            - String specifying the path of the file to copy to.
     */
    public static void copyFile(String fromFile, String toFile) throws IOException {
        copyFile(new FileInputStream(fromFile), new FileOutputStream(toFile));
    }

    /**
     * Creates the specified <i><b>toFile</b></i> that is a byte for byte a copy
     * of <i><b>fromFile</b></i>. If <i><b>toFile</b></i> already existed, then
     * it will be replaced with a copy of <i><b>fromFile</b></i>. The name and
     * path of <i><b>toFile</b></i> will be that of <i><b>toFile</b></i>. Both
     * <i><b>fromFile</b></i> and <i><b>toFile</b></i> will be closed by this
     * operation.
     * 
     * @param fromFile
     *            - File for the file to copy from.
     * @param toFile
     *            - File for the file to copy to.
     */
    public static void copyFile(File fromFile, File toFile) throws IOException {
        copyFile(new FileInputStream(fromFile), new FileOutputStream(toFile));
    }

    /**
     * Creates the specified <i><b>toFile</b></i> that is a byte for byte a copy
     * of <i><b>fromFile</b></i>. If <i><b>toFile</b></i> already existed, then
     * it will be replaced with a copy of <i><b>fromFile</b></i>. The name and
     * path of <i><b>toFile</b></i> will be that of <i><b>toFile</b></i>. Both
     * <i><b>fromFile</b></i> and <i><b>toFile</b></i> will be closed by this
     * operation.
     * 
     * @param fromFile
     *            - FileInputStream for the file to copy from.
     * @param toFile
     *            - FileInputStream for the file to copy to.
     */
    public static void copyFile(FileInputStream fromFile, FileOutputStream toFile) throws IOException {
        FileChannel fromChannel = fromFile.getChannel();
        FileChannel toChannel = toFile.getChannel();

        try {
            fromChannel.transferTo(0, fromChannel.size(), toChannel);
        } finally {
            try {
                if (fromChannel != null) {
                    fromChannel.close();
                }
            } finally {
                if (toChannel != null) {
                    toChannel.close();
                }
            }
        }
    }

    /**
     * Parses a file containing sql statements into a String array that contains
     * only the sql statements. Comments and white spaces in the file are not
     * parsed into the String array. Note the file must not contained malformed
     * comments and all sql statements must end with a semi-colon ";" in order
     * for the file to be parsed correctly. The sql statements in the String
     * array will not end with a semi-colon ";".
     * 
     * @param sqlFile
     *            - String containing the path for the file that contains sql
     *            statements.
     * 
     * @return String array containing the sql statements.
     */
    public static String[] parseSqlFile(String sqlFile) throws IOException {
        return parseSqlFile(new BufferedReader(new FileReader(sqlFile)));
    }

    /**
     * Parses a file containing sql statements into a String array that contains
     * only the sql statements. Comments and white spaces in the file are not
     * parsed into the String array. Note the file must not contained malformed
     * comments and all sql statements must end with a semi-colon ";" in order
     * for the file to be parsed correctly. The sql statements in the String
     * array will not end with a semi-colon ";".
     * 
     * @param sqlFile
     *            - InputStream for the file that contains sql statements.
     * 
     * @return String array containing the sql statements.
     */
    public static String[] parseSqlFile(InputStream sqlFile) throws IOException {
        return parseSqlFile(new BufferedReader(new InputStreamReader(sqlFile)));
    }

    /**
     * Parses a file containing sql statements into a String array that contains
     * only the sql statements. Comments and white spaces in the file are not
     * parsed into the String array. Note the file must not contained malformed
     * comments and all sql statements must end with a semi-colon ";" in order
     * for the file to be parsed correctly. The sql statements in the String
     * array will not end with a semi-colon ";".
     * 
     * @param sqlFile
     *            - Reader for the file that contains sql statements.
     * 
     * @return String array containing the sql statements.
     */
    public static String[] parseSqlFile(Reader sqlFile) throws IOException {
        return parseSqlFile(new BufferedReader(sqlFile));
    }

    /**
     * Parses a file containing sql statements into a String array that contains
     * only the sql statements. Comments and white spaces in the file are not
     * parsed into the String array. Note the file must not contained malformed
     * comments and all sql statements must end with a semi-colon ";" in order
     * for the file to be parsed correctly. The sql statements in the String
     * array will not end with a semi-colon ";".
     * 
     * @param sqlFile
     *            - BufferedReader for the file that contains sql statements.
     * 
     * @return String array containing the sql statements.
     */
    public static String[] parseSqlFile(BufferedReader sqlFile) throws IOException {
        String line;
        StringBuilder sql = new StringBuilder();
        String multiLineComment = null;

        while ((line = sqlFile.readLine()) != null) {
            line = line.trim();

            // Check for start of multi-line comment
            if (multiLineComment == null) {
                // Check for first multi-line comment type
                if (line.startsWith("/*")) {
                    if (!line.endsWith("}")) {
                        multiLineComment = "/*";
                    }
                // Check for second multi-line comment type
                } else if (line.startsWith("{")) {
                    if (!line.endsWith("}")) {
                        multiLineComment = "{";
                }
                // Append line if line is not empty or a single line comment
                } else if (!line.startsWith("--") && !line.equals("")) {
                    sql.append(line);
                } // Check for matching end comment
            } else if (multiLineComment.equals("/*")) {
                if (line.endsWith("*/")) {
                    multiLineComment = null;
                }
            // Check for matching end comment
            } else if (multiLineComment.equals("{")) {
                if (line.endsWith("}")) {
                    multiLineComment = null;
                }
            }

        }

        sqlFile.close();

        return sql.toString().split(";");
    }

}

194
2018-01-26 21:22



Die SQLiteAssetHelper-Bibliothek macht diese Aufgabe sehr einfach.

Es ist leicht hinzuzufügen als eine Gradle-Abhängigkeit (aber ein Jar ist auch für Ant / Eclipse verfügbar), und zusammen mit der Dokumentation kann es gefunden werden:
https://github.com/jgilfelt/android-sqlite-asset-helper

Wie in der Dokumentation erläutert:

  1. Fügen Sie die Abhängigkeit zu der Gradle-Build-Datei Ihres Moduls hinzu:

    dependencies {
        compile 'com.readystatesoftware.sqliteasset:sqliteassethelper:+'
    }
    
  2. Kopieren Sie die Datenbank in das Verzeichnis acres in einem Unterverzeichnis namens assets/databases. Zum Beispiel:
    assets/databases/my_database.db

    (Optional können Sie die Datenbank in einer Zip-Datei komprimieren, z. B. assets/databases/my_database.zip. Dies ist nicht notwendig, da die APK bereits als Ganzes komprimiert ist.)

  3. Erstellen Sie eine Klasse, zum Beispiel:

    public class MyDatabase extends SQLiteAssetHelper {
    
        private static final String DATABASE_NAME = "my_database.db";
        private static final int DATABASE_VERSION = 1;
    
        public MyDatabase(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }
    }
    

126
2017-08-03 21:08



Ich schätze den besten und neuesten Weg bis heute SQLiteAssetHelper Klasse.

Dieses Tutorial führt Sie perfekt durch Importieren und Verwenden der externen Datenbank in Android

Das Android SQLiteAssetHelper Bibliothek ermöglicht es Ihnen, Ihre zu bauen SQLite   Datenbank in Ihrem Desktop - Computer, und importieren und verwenden Sie es in Ihrem   Android-Anwendung Lassen Sie uns eine einfache Anwendung zum Demonstrieren erstellen   die Anwendung dieser Bibliothek.

Schritt 1: Erstellen Sie eine Datenbank quotes.db mit Ihrer bevorzugten SQLite   Datenbankanwendung (DB Browser for SQLite ist ein tragbares Kreuz   Platform Freeware, mit der SQLite erstellt und bearbeitet werden kann   Datenbanken). Erstellen Sie eine Tabelle 'Anführungszeichen' mit einer einzigen Spalte 'Anführungszeichen'.   Fügen Sie einige zufällige Anführungszeichen in die Tabelle 'Anführungszeichen' ein.

Schritt 2: Die Datenbank kann entweder direkt in das Projekt importiert werden   Es ist oder als eine komprimierte Datei. Die komprimierte Datei wird empfohlen, wenn   Ihre Datenbank ist zu groß. Sie können entweder a erstellen ZIP   Komprimierung oder a GZ Kompression.

Der Dateiname der komprimierten DB-Datei muss sein quotes.db.zip, wenn du   verwenden ZIP-Komprimierung oder quotes.db.gz, wenn Sie GZ benutzen   Kompression.

Schritt 3: Erstellen Sie eine neue Anwendung External Database Demo mit einem   Paketnamen com.javahelps.com.javahelps.externaldatabasedemo.

Schritt 4: Öffne das build.gradle (Modul: app) Datei und fügen Sie Folgendes hinzu   Abhängigkeit.

dependencies {
    compile 'com.readystatesoftware.sqliteasset:sqliteassethelper:+'
}

Sobald du das gespeichert hast build.gradle Datei klicken Sie auf "Jetzt synchronisieren"   Link zum Aktualisieren des Projekts. Sie können das synchronisieren build.gradle, durch   Rechtsklick auf den build.gradleDatei und Auswahl Synchronize build.gradle Option auch.

Schritt 5: Klicken Sie mit der rechten Maustaste auf den App-Ordner und erstellen Sie einen neuen Asset-Ordner.

Schritt 6: Erstellen Sie einen neuen Ordner "Datenbanken" im Ordner "Assets".

Schritt 7: Kopiere und füge die quotes.db.zip Datei in der    assets/databases Mappe.

Schritt 8: Erstellen Sie eine neue Klasse DatabaseOpenHelper

package com.javahelps.externaldatabasedemo;

import android.content.Context;

import com.readystatesoftware.sqliteasset.SQLiteAssetHelper;

public class DatabaseOpenHelper extends SQLiteAssetHelper {
    private static final String DATABASE_NAME = "quotes.db";
    private static final int DATABASE_VERSION = 1;

    public DatabaseOpenHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }
}  Notice that rather than extending SQLiteOpenHelper, the DatabaseOpenHelper extends  SQLiteAssetHelper class.

Schritt 9: Erstellen Sie eine neue Klasse DatabaseAccess und geben Sie den Code wie gezeigt ein   unten. Weitere Details zu dieser Klasse finden Sie unter Advanced Android   Datenbank-Tutorial

package com.javahelps.externaldatabasedemo;

import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

import java.util.ArrayList;
import java.util.List;

public class DatabaseAccess {
    private SQLiteOpenHelper openHelper;
    private SQLiteDatabase database;
    private static DatabaseAccess instance;

    /**
     * Private constructor to aboid object creation from outside classes.
     *
     * @param context
     */
    private DatabaseAccess(Context context) {
        this.openHelper = new DatabaseOpenHelper(context);
    }

    /**
     * Return a singleton instance of DatabaseAccess.
     *
     * @param context the Context
     * @return the instance of DabaseAccess
     */
    public static DatabaseAccess getInstance(Context context) {
        if (instance == null) {
            instance = new DatabaseAccess(context);
        }
        return instance;
    }

    /**
     * Open the database connection.
     */
    public void open() {
        this.database = openHelper.getWritableDatabase();
    }

    /**
     * Close the database connection.
     */
    public void close() {
        if (database != null) {
            this.database.close();
        }
    }

    /**
     * Read all quotes from the database.
     *
     * @return a List of quotes
     */
    public List<String> getQuotes() {
        List<String> list = new ArrayList<>();
        Cursor cursor = database.rawQuery("SELECT * FROM quotes", null);
        cursor.moveToFirst();
        while (!cursor.isAfterLast()) {
            list.add(cursor.getString(0));
            cursor.moveToNext();
        }
        cursor.close();
        return list;
    }
}  In this class only the `getQuotes` method is implemented to read the data from the database. You have the full freedom to insert,

Aktualisieren und löschen Sie alle Zeilen in der Datenbank wie gewohnt. Für mehr Details,   folgen Sie diesem Link Erweiterte Android Datenbank.

Alle datenbankbezogenen Einstellungen sind abgeschlossen und jetzt müssen wir es tun   ein ... kreieren ListView um die Anführungszeichen anzuzeigen.

Schritt 10: Füge hinzu ein ListView in deinem activity_main.xml.

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity">

    <ListView
        android:id="@+id/listView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center" />
</FrameLayout>  

Schritt 11: Finde das Objekt von ListView in dem onCreate Methode von MainActivity und füttern Sie die Zitate, die gelesen werden   Bilden Sie die Datenbank.

package com.javahelps.externaldatabasedemo;

import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.widget.ArrayAdapter;
import android.widget.ListView;

import java.util.List;


public class MainActivity extends ActionBarActivity {
    private ListView listView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        this.listView = (ListView) findViewById(R.id.listView);
        DatabaseAccess databaseAccess = DatabaseAccess.getInstance(this);
        databaseAccess.open();
        List<String> quotes = databaseAccess.getQuotes();
        databaseAccess.close();

        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,
quotes);
        this.listView.setAdapter(adapter);
    }
}

Schritt 12: Speichern Sie alle Änderungen und führen Sie die Anwendung aus.

Zusätzlich zu diesem Artikel können Sie herunterladen SQLiteAssetHelper  Hier


23
2018-01-21 08:01



Meine Lösung verwendet weder eine Bibliothek von Drittanbietern noch zwingt Sie Sie, benutzerdefinierte Methoden aufzurufen SQLiteOpenHelper Unterklasse, um die Datenbank bei der Erstellung zu initialisieren. Es kümmert sich auch um Datenbank-Upgrades. Alles, was getan werden muss, ist Unterklasse SQLiteOpenHelper.

Voraussetzung:

  1. Die Datenbank, die Sie mit der App versenden möchten. Es sollte enthalten eine 1x1-Tabelle mit dem Namen android_metadata mit einem Attribut locale den Wert haben en_US Zusätzlich zu den für Ihre App eindeutigen Tabellen.

Unterklassifizierung SQLiteOpenHelper:

  1. Unterklasse SQLiteOpenHelper.
  2. Ein ... kreieren private Methode innerhalb der SQLiteOpenHelper Unterklasse. Diese Methode enthält die Logik zum Kopieren von Datenbankinhalten aus der Datenbankdatei im Ordner 'Assets' in die Datenbank, die im Kontext des Anwendungspakets erstellt wurde.
  3. Überschreiben onCreate, onUpgrade  und  onOpen Methoden von SQLiteOpenHelper.

Genug gesagt. Hier geht das SQLiteOpenHelper Unterklasse:

public class PlanDetailsSQLiteOpenHelper extends SQLiteOpenHelper {
    private static final String TAG = "SQLiteOpenHelper";

    private final Context context;
    private static final int DATABASE_VERSION = 1;
    private static final String DATABASE_NAME = "my_custom_db";

    private boolean createDb = false, upgradeDb = false;

    public PlanDetailsSQLiteOpenHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
        this.context = context;
    }

    /**
     * Copy packaged database from assets folder to the database created in the
     * application package context.
     * 
     * @param db
     *            The target database in the application package context.
     */
    private void copyDatabaseFromAssets(SQLiteDatabase db) {
        Log.i(TAG, "copyDatabase");
        InputStream myInput = null;
        OutputStream myOutput = null;
        try {
            // Open db packaged as asset as the input stream
            myInput = context.getAssets().open("path/to/shipped/db/file");

            // Open the db in the application package context:
            myOutput = new FileOutputStream(db.getPath());

            // Transfer db file contents:
            byte[] buffer = new byte[1024];
            int length;
            while ((length = myInput.read(buffer)) > 0) {
                myOutput.write(buffer, 0, length);
            }
            myOutput.flush();

            // Set the version of the copied database to the current
            // version:
            SQLiteDatabase copiedDb = context.openOrCreateDatabase(
                DATABASE_NAME, 0, null);
            copiedDb.execSQL("PRAGMA user_version = " + DATABASE_VERSION);
            copiedDb.close();

        } catch (IOException e) {
            e.printStackTrace();
            throw new Error(TAG + " Error copying database");
        } finally {
            // Close the streams
            try {
                if (myOutput != null) {
                    myOutput.close();
                }
                if (myInput != null) {
                    myInput.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
                throw new Error(TAG + " Error closing streams");
            }
        }
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        Log.i(TAG, "onCreate db");
        createDb = true;
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        Log.i(TAG, "onUpgrade db");
        upgradeDb = true;
    }

    @Override
    public void onOpen(SQLiteDatabase db) {
        Log.i(TAG, "onOpen db");
        if (createDb) {// The db in the application package
            // context is being created.
            // So copy the contents from the db
            // file packaged in the assets
            // folder:
            createDb = false;
            copyDatabaseFromAssets(db);

        }
        if (upgradeDb) {// The db in the application package
            // context is being upgraded from a lower to a higher version.
            upgradeDb = false;
            // Your db upgrade logic here:
        }
    }
}

Um schließlich eine Datenbankverbindung zu erhalten, rufen Sie einfach an getReadableDatabase() oder getWritableDatabase() auf der SQLiteOpenHelper Unterklasse und es wird darauf geachtet, eine db zu erstellen, kopieren db Inhalte aus der angegebenen Datei im Ordner 'Assets', wenn die Datenbank nicht existiert.

Kurz gesagt, Sie können das verwenden SQLiteOpenHelper Englisch: www.weisang.info/index.php?id=143&t...h=ddb8dcdcfc Unterklasse für den Zugriff auf die Datenbank, die im Ordner Assets enthalten ist, genau wie für eine Datenbank, die mit SQL - Abfragen in der Datenbank initialisiert wird onCreate() Methode.


12
2018-03-26 14:56



Versenden der App mit einer Datenbankdatei in Android Studio 3.0

Das Versenden der App mit einer Datenbankdatei ist eine gute Idee für mich. Der Vorteil ist, dass Sie keine komplexe Initialisierung durchführen müssen, was manchmal viel Zeit kostet, wenn Ihr Datensatz sehr umfangreich ist.

Schritt 1: Vorbereiten der Datenbankdatei

Halten Sie Ihre Datenbank bereit. Es kann entweder eine .db-Datei oder eine .sqlite-Datei sein. Wenn Sie eine .sqlite-Datei verwenden, müssen Sie nur die Namen der Dateierweiterungen ändern. Die Schritte sind gleich.

In diesem Beispiel habe ich eine Datei namens testDB.db vorbereitet. Es enthält eine Tabelle und einige Beispieldaten enter image description here

Schritt 2: Importieren Sie die Datei in Ihr Projekt

Erstellen Sie den Ordner "Assets", wenn Sie noch keinen haben. Kopieren Sie dann die Datenbankdatei und fügen Sie sie in diesen Ordner ein

enter image description here

Schritt 3: Kopieren Sie die Datei in den Datenordner der App

Sie müssen die Datenbankdatei in den Datenordner der App kopieren, um weitere Interaktion mit ihr zu ermöglichen. Dies ist eine einmalige Aktion (Initialisierung) zum Kopieren der Datenbankdatei. Wenn Sie diesen Code mehrmals aufrufen, wird die Datenbankdatei im Datenordner durch die Datei im Ordner "Assets" überschrieben. Dieser Überschreibvorgang ist nützlich, wenn Sie die Datenbank während des App-Updates in Zukunft aktualisieren möchten.

Beachten Sie, dass diese Datenbankdatei während des App-Updates nicht im Datenordner der App geändert wird. Nur die Deinstallation wird es löschen.

Die Datenbankdatei muss kopiert werden /databases Mappe. Öffnen Sie den Gerätedatei-Explorer. Eingeben data/data/<YourAppName>/ Lage. Dies ist der oben erwähnte Standard-Datenordner der App. Und standardmäßig wird die Datenbankdatei in einem anderen Ordner namens Datenbanken in diesem Verzeichnis abgelegt

enter image description here

Jetzt ist der Prozess der Kopierdatei ziemlich ähnlich dem, was Java macht. Verwenden Sie den folgenden Code zum Kopieren. Dies ist der Initiierungscode. Es kann auch verwendet werden, um die Datenbankdatei in Zukunft zu aktualisieren (durch Überschreiben).

//get context by calling "this" in activity or getActivity() in fragment
//call this if API level is lower than 17  String appDataPath = "/data/data/" + context.getPackageName() + "/databases/"
String appDataPath = context.getApplicationInfo().dataDir;

File dbFolder = new File(appDataPath + "/databases");//Make sure the /databases folder exists
dbFolder.mkdir();//This can be called multiple times.

File dbFilePath = new File(appDataPath + "/databases/testDB.db");

try {
    InputStream inputStream = context.getAssets().open("testDB.db");
    OutputStream outputStream = new FileOutputStream(dbFilePath);
    byte[] buffer = new byte[1024];
    int length;
    while ((length = inputStream.read(buffer))>0)
    {
        outputStream.write(buffer, 0, length);
    }
    outputStream.flush();
    outputStream.close();
    inputStream.close();
} catch (IOException e){
    //handle
}

Aktualisieren Sie dann den Ordner, um den Kopiervorgang zu überprüfen

enter image description here

Schritt 4: Erstellen Sie einen geöffneten Helper für die Datenbank

Erstellen Sie eine Unterklasse für SQLiteOpenHelper, mit connect, close, path usw. habe ich es genannt DatabaseOpenHelper

import android.content.Context;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class DatabaseOpenHelper extends SQLiteOpenHelper {
    public static final String DB_NAME = "testDB.db";
    public static final String DB_SUB_PATH = "/databases/" + DB_NAME;
    private static String APP_DATA_PATH = "";
    private SQLiteDatabase dataBase;
    private final Context context;

    public DatabaseOpenHelper(Context context){
        super(context, DB_NAME, null, 1);
        APP_DATA_PATH = context.getApplicationInfo().dataDir;
        this.context = context;
    }

    public boolean openDataBase() throws SQLException{
        String mPath = APP_DATA_PATH + DB_SUB_PATH;
        //Note that this method assumes that the db file is already copied in place
        dataBase = SQLiteDatabase.openDatabase(mPath, null, SQLiteDatabase.OPEN_READWRITE);
        return dataBase != null;
    }

    @Override
    public synchronized void close(){
        if(dataBase != null) {dataBase.close();}
        super.close();
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }
}

Schritt 5: Erstellen Sie eine Top-Level-Klasse für die Interaktion mit der Datenbank

Dies ist die Klasse, die Ihre Datenbankdatei liest und schreibt. Außerdem gibt es eine Beispielabfrage, um den Wert in der Datenbank auszudrucken.

import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;

public class Database {
    private final Context context;
    private SQLiteDatabase database;
    private DatabaseOpenHelper dbHelper;

    public Database(Context context){
        this.context = context;
        dbHelper = new DatabaseOpenHelper(context);
    }

    public Database open() throws SQLException
    {
        dbHelper.openDataBase();
        dbHelper.close();
        database = dbHelper.getReadableDatabase();
        return this;
    }

    public void close()
    {
        dbHelper.close();
    }

    public void test(){
        try{
            String query ="SELECT value FROM test1";
            Cursor cursor = database.rawQuery(query, null);
            if (cursor.moveToFirst()){
                do{
                    String value = cursor.getString(0);
                    Log.d("db", value);
                }while (cursor.moveToNext());
            }
            cursor.close();
        } catch (SQLException e) {
            //handle
        }
    }
}

Schritt 6: Testlauf

Testen Sie den Code, indem Sie die folgenden Codezeilen ausführen.

Database db = new Database(context);
db.open();
db.test();
db.close();

Schlage den Run Button und juble!

enter image description here


8
2017-11-07 20:23



Im November 2017 veröffentlichte Google den Raumpersistenzbibliothek

Aus der Dokumentation:

Die Raumpersistenzbibliothek stellt eine Abstraktionsschicht über SQLite bereit   um einen reibungslosen Datenbankzugriff zu ermöglichen und gleichzeitig die volle Leistung von   SQLite.

Mithilfe der Bibliothek können Sie einen Cache für die Daten Ihrer App auf einem Gerät erstellen   Damit wird deine App ausgeführt. Dieser Cache dient als Single Ihrer App   Quelle der Wahrheit, ermöglicht Benutzern, eine konsistente Kopie des Schlüssels anzuzeigen   Informationen in Ihrer App, unabhängig davon, ob Benutzer eine   Internetverbindung.

Die Room-Datenbank hat einen Rückruf, wenn die Datenbank zum ersten Mal erstellt oder geöffnet wird. Sie können den Rückruf zum Erstellen verwenden, um Ihre Datenbank zu füllen.

Room.databaseBuilder(context.applicationContext,
        DataDatabase::class.java, "Sample.db")
        // prepopulate the database after onCreate was called
        .addCallback(object : Callback() {
            override fun onCreate(db: SupportSQLiteDatabase) {
                super.onCreate(db)
                // moving to a new thread
                ioThread {
                    getInstance(context).dataDao()
                                        .insert(PREPOPULATE_DATA)
                }
            }
        })
        .build()

Code von diesem Blogeintrag.


7
2017-11-28 17:21



Nach dem, was ich gesehen habe, sollten Sie eine Datenbank liefern, die bereits die Tabellen Setup und Daten enthält. Wenn Sie jedoch möchten (und abhängig von der Art der Anwendung, die Sie haben) können Sie "Upgrade-Datenbank-Option" zulassen. Dann laden Sie die neueste sqlite-Version herunter, holen sich die neuesten Insert / Create-Anweisungen einer online gehosteten Textdatei, führen die Anweisungen aus und führen eine Datenübertragung von der alten zu der neuen db durch.


5
2018-02-04 20:14



Endlich habe ich es gemacht!! Ich habe diesen Link benutzt help Verwenden Sie Ihre eigene SQLite-Datenbank in Android-Anwendungen, aber musste es ein bisschen ändern.

  1. Wenn Sie viele Pakete haben, sollten Sie den Master-Paketnamen hier angeben:

    private static String DB_PATH = "data/data/masterPakageName/databases";

  2. Ich habe die Methode geändert, die die Datenbank vom lokalen Ordner in den Emulatorordner kopiert! Es gab ein Problem, wenn dieser Ordner nicht existierte. Also zuerst sollte es den Pfad überprüfen und wenn es nicht da ist, sollte es den Ordner erstellen.

  3. Im vorherigen Code, der copyDatabase Methode wurde nie aufgerufen, wenn die Datenbank nicht existiert und die checkDataBase Methode verursachte Ausnahme. Also habe ich den Code ein wenig geändert.

  4. Wenn Ihre Datenbank keine Dateierweiterung hat, verwenden Sie den Dateinamen nicht mit einem Dateinamen.

es funktioniert gut für mich, ich hoffe, es könnte auch für Sie nützlich sein

    package farhangsarasIntroduction;


import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;

import android.content.Context;
import android.database.Cursor;

import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;

import android.util.Log;


    public class DataBaseHelper extends SQLiteOpenHelper{

    //The Android's default system path of your application database.
    private static String DB_PATH = "data/data/com.example.sample/databases";

    private static String DB_NAME = "farhangsaraDb";

    private SQLiteDatabase myDataBase;

    private final Context myContext;

    /**
      * Constructor
      * Takes and keeps a reference of the passed context in order to access to the application assets and resources.
      * @param context
      */
    public DataBaseHelper(Context context) {

        super(context, DB_NAME, null, 1);
            this.myContext = context;

    }   

    /**
      * Creates a empty database on the system and rewrites it with your own database.
      * */
    public void createDataBase() {

        boolean dbExist;
        try {

             dbExist = checkDataBase();


        } catch (SQLiteException e) {

            e.printStackTrace();
            throw new Error("database dose not exist");

        }

        if(dbExist){
        //do nothing - database already exist
        }else{

            try {

                copyDataBase();


            } catch (IOException e) {

                e.printStackTrace();
                throw new Error("Error copying database");

            }
    //By calling this method and empty database will be created into the default system path
    //of your application so we are gonna be able to overwrite that database with our database.
        this.getReadableDatabase();


    }

    }

    /**
      * Check if the database already exist to avoid re-copying the file each time you open the application.
      * @return true if it exists, false if it doesn't
      */
    private boolean checkDataBase(){

    SQLiteDatabase checkDB = null;

    try{
        String myPath = DB_PATH +"/"+ DB_NAME;

        checkDB = SQLiteDatabase.openDatabase(myPath, null, SQLiteDatabase.OPEN_READONLY);
    }catch(SQLiteException e){

    //database does't exist yet.
        throw new Error("database does't exist yet.");

    }

    if(checkDB != null){

    checkDB.close();

    }

    return checkDB != null ? true : false;
    }

    /**
      * Copies your database from your local assets-folder to the just created empty database in the
      * system folder, from where it can be accessed and handled.
      * This is done by transfering bytestream.
      * */
    private void copyDataBase() throws IOException{



            //copyDataBase();
            //Open your local db as the input stream
            InputStream myInput = myContext.getAssets().open(DB_NAME);

            // Path to the just created empty db
            String outFileName = DB_PATH +"/"+ DB_NAME;
            File databaseFile = new File( DB_PATH);
             // check if databases folder exists, if not create one and its subfolders
            if (!databaseFile.exists()){
                databaseFile.mkdir();
            }

            //Open the empty db as the output stream
            OutputStream myOutput = new FileOutputStream(outFileName);

            //transfer bytes from the inputfile to the outputfile
            byte[] buffer = new byte[1024];
            int length;
            while ((length = myInput.read(buffer))>0){
            myOutput.write(buffer, 0, length);
            }

            //Close the streams
            myOutput.flush();
            myOutput.close();
            myInput.close();



    }



    @Override
    public synchronized void close() {

        if(myDataBase != null)
        myDataBase.close();

        super.close();

    }

    @Override
    public void onCreate(SQLiteDatabase db) {

    }



    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }

     you to create adapters for your views.

}

5
2018-02-14 09:03



Momentan gibt es keine Möglichkeit, eine SQLite-Datenbank vorzufertigen, um sie mit Ihrer APK zu versenden. Das Beste, was Sie tun können, ist, die entsprechende SQL als Ressource zu speichern und sie von Ihrer Anwendung aus auszuführen. Ja, dies führt zur Duplizierung von Daten (die gleiche Information existiert als eine Ressource und als eine Datenbank), aber es gibt momentan keinen anderen Weg. Der einzige schadensbegrenzende Faktor ist, dass die apk-Datei komprimiert ist. Meine Erfahrung ist 908KB komprimiert auf weniger als 268KB.

Der folgende Thread hat die beste Diskussion / Lösung, die ich mit gutem Beispielcode gefunden habe.

http://groups.google.com/group/android-developers/msg/9f455ae93a1cf152

Ich habe meine CREATE-Anweisung als String-Ressource gespeichert und mit Context.getString () gelesen und mit SQLiteDatabase.execSQL () ausgeführt.

Ich habe die Daten für meine Inserts in res / raw / inserts.sql gespeichert (ich habe die sql-Datei erstellt, 7000+ Zeilen). Unter Verwendung der Technik von dem obigen Link trat ich in eine Schleife ein, lies die Datei Zeile für Zeile durch und kongrahierte die Daten auf "INSERT INTO tbl VALUE" und führte eine weitere SQLiteDatabase.execSQL () durch. Es ist nicht sinnvoll, 7000 "INSERT INTO tbl VALUE" zu speichern, wenn sie einfach nur konkaviert werden können.

Es dauert ungefähr zwanzig Sekunden auf dem Emulator, ich weiß nicht, wie lange das auf einem echten Telefon dauern würde, aber es passiert nur einmal, wenn der Benutzer die Anwendung zum ersten Mal startet.


4
2018-02-04 23:32