数据存储
来源:互联网 发布:欧莱雅男士面膜 知乎 编辑:程序博客网 时间:2024/06/06 20:14
1.文件存储
文件存储是Android中最基本的一种数据库存储方式,它不对存储的内容进行任何的格式化处理,所有数据都是原封不动的保存到文件当中。因为它比较适用于存储一些简单的文本数据或二进制数据。如果想使用文件存储的方式来保存一些较为复杂的文本数据,就需要定义一套自己的格式规范,这样可以方便之后将数据从文件中重新解析出来。
content类中提供了一个openFileOutput()方法,用于把数据存储到指定的文件中。这个方法接受两个参数,第一个参数是文件名,第二个参数是操作模式。主要有两种模式可选,MODE_PRIVATE和MODE_APPEND。其中MODE_PRIVATE是默认操作模式,表示当指定同样文件名的时候所有写入的内容将会覆盖原文件中的内容,MODE_APPEND表示如果该文件已存在,则往里面追加内容,不存在创建新文件。默认存储到data/data//files/目录下
存储文件
public class MainActivity extends AppCompatActivity { private EditText edit; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); edit = (EditText) findViewById(R.id.edit); } @Override protected void onDestroy() { super.onDestroy(); String inputText = edit.getText().toString(); save(inputText); } public void save(String inputText){ FileOutputStream out = null; BufferedWriter writer = null; try { out = openFileOutput("data", Context.MODE_PRIVATE); writer = new BufferedWriter(new OutputStreamWriter(out)); writer.write(inputText); } catch (IOException e) { e.printStackTrace(); }finally { try { writer.close(); } catch (IOException e) { e.printStackTrace(); } } }}
在onDestroy()方法中我们获取了EditText中输出的内容,并调用save()方法把输入的内容存储到文件中,文件名为data。
可以在Tool —— Android —— Android Device Monitor工具查看,Android7.0
系统模拟器无法正常查看File Explorer中的内容,这或许是模拟器的一个bug。
读取文件
Context类中还提供了一个openFileInput()方法,用于从文件中读取数据。这个方法只接收一个参数,及要读取的文件名,然后系统会自动到data/data//files/目录下去加载这个文件,并返回FileInputStream对象,得到以后再通过Java流的方式就可以读取出来。
public class MainActivity extends AppCompatActivity { private EditText edit; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); edit = (EditText) findViewById(R.id.edit); String inputText = load();//从data文件中读取数据 if(!TextUtils.isEmpty(inputText)){//是否是null edit.setText(inputText);//设置EditText数据 edit.setSelection(inputText.length());//将光标移动到文件的末尾 Toast.makeText(this,"Rostoring succeeded",Toast.LENGTH_SHORT).show(); } } public String load(){ FileInputStream in = null; BufferedReader reader = null; StringBuilder content = new StringBuilder(); try{ in = openFileInput("data"); reader = new BufferedReader(new InputStreamReader(in)); String line = ""; while((line = reader.readLine())!=null){ content.append(line); } }catch (IOException e){ e.printStackTrace(); }finally { { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } return content.toString(); }}
2.SharedPreferences存储
SharedPreferences是采用键值对的方法来存储数据的。并且还支持不同的数据类型存储,如果存储的类型是整型,那么读取出来的就是整型的,如果是字符串,读取出来的就是字符串。
很多时候我们开发的软件需要向用户提供软件参数设置功能,例如我们常用的QQ,用户可以设置是否允许陌生人添加自己为好友,记住密码。对于软件配置参数的保存,如果是window软件通常我们会采用ini文件进行保存,如果是j2se应用,我们会采用properties属性文件或者xml进行保存。如果是Android应用,我们最适合采用什么方式保存软件配置参数呢?Android平台给我们提供了一个SharedPreferences类,它是一个轻量级的存储类,特别适合用于保存软件配置参数。使用SharedPreferences保存数据,其背后是用xml文件存放数据,文件存放在/data/data//shared_prefs目录下
方法1:
Context.getSharedPreferences(文件名称,操作模式)
如果文件不存在则会创建一个。目前只有MODE_PRIVATE 这一种模式可以选。其他几种模式均已被废弃。
方法2:
Activity.getSharedPreferences(操作模式)
使用这个方法会自动将当前活动的类名作为SharedPreferences的文件名
方法3
PreferenceManager.getDefaultSharedPreferences(Context)
使用这个方法会自动使用当前程序的包名作为前缀来命名SharedPreferences文件
主要3步实现:
(1)调用SharedPreferences对象的edit()方法来获取一个SharedPreferences.Editor对象。
(2)向SharedPreferences.Editor对象中添加数据,比如添加一个布尔型数据就使用putBoolean()方法。
(3)调用apply()方法将添加的数据提交。
SharedPreferences存储文件
public class MainActivity extends AppCompatActivity { private EditText edit; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button saveData = (Button) findViewById(R.id.save_data); saveData.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { SharedPreferences.Editor editor = getSharedPreferences("data",MODE_PRIVATE).edit(); editor.putString("name","Tom"); editor.putInt("age",28); editor.putBoolean("married",false); editor.apply(); } }); }}
data/data/com.example.sharedpreferencestest/shared_prefs/目录下的data.xml导出查看:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?><map> <string name="name">Tom</string> <int name="age" value="28" /> <boolean name="married" value="false" /></map>
SharedPreferences读取文件
public class MainActivity extends AppCompatActivity { private EditText edit; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button restoreData = (Button) findViewById(R.id.restore_data); restoreData.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { SharedPreferences pref = getSharedPreferences("data",MODE_PRIVATE); String name = pref.getString("name",""); int age = pref.getInt("age",0); boolean married = pref.getBoolean("married",false); Log.d("MainActivity","namse is "+name+" age is "+age+" married is "+married); } }); }}
logcat:
MainActivity: namse is Tom age is 28 married is false
SharedPreferences实现记住密码功能
activity_login.xml:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <EditText android:id="@+id/account" android:layout_width="match_parent" android:layout_height="wrap_content" /> <EditText android:id="@+id/password" android:layout_width="match_parent" android:layout_height="wrap_content" /> <LinearLayout android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content"> <CheckBox android:id="@+id/remember_pass" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="18sp" android:text="Remember password"/> </LinearLayout> <Button android:id="@+id/login" android:layout_width="match_parent" android:layout_height="60dp" android:text="Remember password"/></LinearLayout>
MainActivity.class:
public class LoginActivity extends AppCompatActivity { private EditText accountEdit;//账户 private EditText passwordEdit;//密码 private Button login;//登录 private CheckBox rememberPass;//记住密码 private SharedPreferences pref; private SharedPreferences.Editor editor; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); pref = PreferenceManager.getDefaultSharedPreferences(this); accountEdit = (EditText) findViewById(R.id.account); passwordEdit = (EditText) findViewById(R.id.password); login = (Button) findViewById(R.id.login); rememberPass = (CheckBox) findViewById(R.id.remember_pass); //如果没有找到相应的值,就会使用方法中第二个参数进行代替 boolean isRemember = pref.getBoolean("remember_password",false); //如果上次选中了记住密码这个选项,那么isRemember就是true if(isRemember){ //将上次保存的数据设置到文本框中,并且选中记住密码复选框 String account = pref.getString("account",""); String password = pref.getString("password",""); accountEdit.setText(account); passwordEdit.setText(password); rememberPass.setChecked(true); } login.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { String account = accountEdit.getText().toString(); String password = passwordEdit.getText().toString(); if(account.equals("admin") && password.equals("123456")){ editor = pref.edit(); //检测记住密码复选框是否被选中,如果选中就存储数据,如果没有选中,就清除数据 if(rememberPass.isChecked()){ editor.putBoolean("remember_password",true); editor.putString("account",account); editor.putString("password",password); }else{ editor.clear(); } //跳转到主页面 Intent intent = new Intent(LoginActivity.this,MainActivity.class); startActivity(intent); finish(); } else { //登录失败 Toast.makeText(LoginActivity.this,"account or password is invalid",Toast.LENGTH_SHORT).show(); } } }); }}
SQLite数据库存储
SQLite数据库
SQLite是一款轻量级的关系型数据库,它的运算速度非常快,占用资源很少,通常只需要几百K的内存就足够了,因此特别适合在移动设备上使用。
SQLite不仅支持标准的SQL语法,还遵守了数据库的 ACID 事务,只要你以前使用过其他的关系型数据库,就可以很快的上手SQLite。
而SQLite又比一般的数据库要简单的多,它甚至不用设置用户名和密码就可以使用。
Android 正是把这个功能极为强大的数据库嵌入到了系统当中,使得本地持久化的功能有了一次质的飞跃。
创建数据库
Android 提供了一个SQLiteOpenHelper 帮助类,借助这个类就可以非常简单的对数据库进行创建和升级。
SQLiteOpenHelper 是一个抽象类,如果需要使用它的话,就需要创建一个帮助类去继承它。
SQLiteOpenHelper 中有两个抽象方法,分别是 onCreate() 和 onUpdate(),需要在帮助类里重写这两个方法,然后分别在这两个方法中去实现创建和升级数据库的逻辑。
SQLiteOpenHelper 中还有两个非常重要的实例方法,getReadableDatabase() 和 getWritableDatabase()。这两种方法都可以创建或打开一个现有的数据库(如果数据库已存在则直接打开,否则创建一个新的数据库),并返回一个可对数据库进行读写操作的对象。不同的是,当数据库不可写入的时候(如磁盘空间已满)getReadableDatabase() 方法返回的对象将以只读的方式去打开数据库,而getWritableDatabase() 方法将抛出异常。
SQLiteOpenHelper 的构造方法接收四个参数,第一个参数是 Context,必须要有Context对象才能对数据库进行操作。第二个参数是数据库名,创建数据库时使用的就是这里指定的名称。第三个参数允许在查询数据库的时候返回一个自定义的 Cursor,一般传入null。第四个参数表示当前数据库的版本号,可用于对数据库进行升级操作。
构建出 SQLiteOpenHelper 的实例之后,再调用它的 getReadableDatabase() 或 getWritableDatabase() 方法就能够创建数据库了,数据库文件会存放在 /data/data/<包名>/database/ 目录下。
Android中的数据库存储是直接使用了SQLite。在android应用中创建数据库后数据库文件是存储在/data/ data/应用包名/databases/下。
在Android中使用到SQLite会涉及到以下三个类或接口:
1.SQLiteOpenHelper
*SQLiteOpenHelper 构造方法,一般传递一个要创建的数据库名称name参数
*onCreate 创建数据库时调用
*onUpgrade 版本更新时调用
*getReadableDatabase 创建或打开一个只读数据库
*getWritableDatabase 创建或打开一个读写数据库
2.SQLiteDatabase
*openOrCreateDatabase 打开或者创建数据库
*insert 添加一条记录
*delete 删除一条记录
*query 查询记录
*update 更新记录
*execSQL 执行一条SQL语句
*close 关闭数据库
3.Cursor
*getCount 总记录条数
*isFirst 判断是否第一条记录
*isLast 判断是否最后一条记录
*moveToFirst 移动到第一条记录
*moveToLast 移动到最后一条记录
*move 移动到指定记录
*moveToNext 移动到下一条记录
*moveToPrevious 移动到上一条记录
*getColumnIndexOrThrow根据列名称获得列索引
*getInt 获得指定列索引的int类型值
*getString 获得指定列索引的String类型值
创建一个数据库
activity_main.xml
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:id="@+id/create_database" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Create database"/> <Button android:id="@+id/add_data" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Add data"/> <Button android:id="@+id/update_data" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Update data"/> <Button android:id="@+id/delete_data" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Delete data"/> <Button android:id="@+id/query_data" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Query data"/></LinearLayout>
MyDatabaseHelper.class
public class MyDatabaseHelpter extends SQLiteOpenHelper { public static final String CREATE_BOOK = "create table Book ("+ "id integer primary key autoincrement, "+ "author text, "+ "price real, "+ "pages integer, "+ "name text)"; public static final String CREATE_CATEGORY = "create table Category ("+ "id integer primary key autoincrement, "+ "category_name text, "+ "category_code integer)"; private Context mContext; public MyDatabaseHelpter(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) { super(context, name, factory, version); mContext = context; } public MyDatabaseHelpter(Context context, String name) { super(context, name, null,1); } @Override public void onCreate(SQLiteDatabase db) { //执行建表语句 db.execSQL(CREATE_BOOK); db.execSQL(CREATE_CATEGORY); Toast.makeText(mContext,"Create succeeded",Toast.LENGTH_SHORT).show(); } //如果想要这个方法执行,就必须更新数据库的版本号 @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { //如果已经存在这个表则删除 db.execSQL("drop table if exists Book"); db.execSQL("drop table if exists Category"); //重新创建 onCreate(db); }}
MainActivity.class
public class MainActivity extends AppCompatActivity implements View.OnClickListener{ private MyDatabaseHelpter dbHelper; private SQLiteDatabase db; private long id; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //升级数据库必须更新版本号 dbHelper = new MyDatabaseHelpter(this,"BookStore.db",null,1); Button createDatabase = (Button) findViewById(R.id.create_database); Button addButton = (Button) findViewById(R.id.add_data); Button updateButton = (Button) findViewById(R.id.update_data); Button deleteButton = (Button) findViewById(R.id.delete_data); Button queryButton = (Button) findViewById(R.id.query_data); createDatabase.setOnClickListener(this); addButton.setOnClickListener(this); updateButton.setOnClickListener(this); deleteButton.setOnClickListener(this); queryButton.setOnClickListener(this); } @Override public void onClick(View view) { ContentValues values = new ContentValues(); switch (view.getId()){ case R.id.create_database: dbHelper.getReadableDatabase(); break; case R.id.add_data: //SQLiteDatabase的insert(String table,String nullColumnHack,ContentValues values) //参数1 表名称 //参数2 空列的默认值 //参数3 ContentValues类型的一个封装了列名称和列值的Map db = dbHelper.getWritableDatabase(); values.clear(); //开始组装第一条数据 //id是自增长,所以没有给它赋值 values.put("name","The Da Vinci Code"); values.put("author","Dan Brown"); values.put("pages",454); values.put("price",16.69); db.insert("Book", null, values); break; case R.id.update_data: //调用SQLiteDatabase的update(String table, ContentValues values, String whereClause, String[] whereArgs) //参数1 表名称 //参数2 ContentValues对象 //参数3 更新条件(where字句) //参数4 更新条件数组 db = dbHelper.getWritableDatabase(); values.clear(); values.put("price",10.99); db.update("Book", values, "id = ?",new String[]{id+""}); break; case R.id.delete_data: //调用SQLiteDatabase的delete(String table,String whereClause,String[] whereArgs) //参数1 表名称 //参数2 删除条件 //参数3 删除条件值数组 db = dbHelper.getWritableDatabase(); db.delete("Book","id > ?",new String[]{id+""}); break; case R.id.query_data: //public Cursor query(String table, String[] columns, String selection, String[] selectionArgs, // String groupBy, String having, String orderBy, String limit); //参数table:表名称 //参数columns:列名称数组 //参数selection:条件字句,相当于where //参数selectionArgs:条件字句,参数数组 //参数groupBy:分组列 //参数having:分组条件 //参数orderBy:排序列 //参数limit:分页查询限制 //参数Cursor:返回值,相当于结果集ResultSet //Cursor是一个游标接口,提供了遍历查询结果的方法,如移动指针方法move(),获得列值方法getString()等. db = dbHelper.getWritableDatabase(); Cursor cursor = db.query("Book", null, null, null, null, null, null, null); if(cursor.moveToFirst()){//移动到第一条记录 do{ //遍历Cursor对象,取出数据打印 String name = cursor.getString(cursor.getColumnIndex("name")); String author = cursor.getString(cursor.getColumnIndex("author")); int pages = cursor.getInt(cursor.getColumnIndex("price")); double price = cursor.getDouble(cursor.getColumnIndex("price")); Log.d("MainActivity","book name is "+name+" ,author is "+author+ " ,pages is "+pages+" ,price is "+price); }while(cursor.moveToNext());//移动到下一条记录 } cursor.close(); break; default: break; //添加数据的方法 //增加 /*db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)", new String[]{"The Da Vinci Code","Dan Brown","454","16.96"}); //更新 db.execSQL("update Book set price = ? where name = ?", new String[]{"10.99","The Da Vinci Code"}); //删除 db.execSQL("delete from Book where pages > ?",new String[]{"500"}); //查询 db.rawQuery("select * from Book",null);*/ } }}
在onCreate()方法中创建了一个MyDatabaseHelper对象,并且通过构造函数的参数将数据库指定为BookStore.db,版本号指定为1,然后再Create database 按钮的点击事件里调用了getWritableDatabase()方法。这样当第一次点击Create database按钮时,就会检测到当前程序中并没有BookStore.db这个数据库,于是会创建该数据库并调用MyDatabasHelpter中的onCreate()方法,这样Book表也就得到了创建,然后会弹出Toast提示创建成功。再次点击Create database按钮时,会发现此时已经存在BookStore.db数据库了,就不会再创建一次。
LitePal
配置Litepal开源项目:
现在大多数开源项目都会将版本提交到jcenter上,我们只需要在app/build.gradle文件中声明该开源库的引用就行了。
compile 'org.litepal.android:core:1.4.1'
dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) compile 'com.android.support:appcompat-v7:26.+' compile 'com.android.support.constraint:constraint-layout:1.0.2' compile 'org.litepal.android:core:1.4.1' testCompile 'junit:junit:4.12'}
然后在project/app/src/main 目录右键-New-Directory,创建一个assets目录,然后在目录下新建一个litepal.xml文件。
<?xml version="1.0" encoding="UTF-8" ?><litepal> <dbname value="BookStore"></dbname> <version value="1"></version> <list> <mapping class="com.example.litepaltest.Book"></mapping> </list></litepal>
dbname是数据库的名字
version是数据库的版本号
list是数据库的映射模型(数据库表)
mapping是数据库的映射模型的地址(数据库表结构)
AndroidManifest.xml
配置LitepalApplication,由于操作数据库时需要用到Context,而我们显然不希望在每个接口中都去传一遍这个参数,那样操作数据库就显得太繁琐了。因此,LitePal使用了一个方法来简化掉Context这个参数,只需要在AndroidManifest.xml中配置一下LitePalApplication,LitePal就能在内部自动获取到Context了。
配置如下:
<application android:name="org.litepal.LitePalApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" ......
也可以创建自己的Application,这样可以全局得到Context
//也可以继承LitePalApplicationpublic class MyApplication extends Application { private static Context context; @Override public void onCreate() { super.onCreate(); context = getApplicationContext(); //这种写法就相当于我们把全局的Context对象通过参数传递给了LitePal,效果和在 //AndroidManifest.xml中配置LitePalApplication是一模一样。 LitePal.initialize(context); } public static Context getContext(){ return context; }}
Book.class:
public class Book extends DataSupport{ private int id; private String author; private double price; private int pages; private String name; private String press; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } public int getPages() { return pages; } public void setPages(int pages) { this.pages = pages; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPress() { return press; } public void setPress(String press) { this.press = press; }}
LitePal采取的是对象关系映射(ORM)的模式。我们使用的编程语言是面向对象语言,而使用的数据库则是关系型数据库,那么将面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是对象关系映射了。
Book.class 对应数据库中的Book表,里面的id,author,price,pages,name几个字段,对应了表中的每一个列。
必须继承DataSupport。
MainActivity.class
public class MainActivity extends AppCompatActivity implements View.OnClickListener{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button createDatabase = (Button) findViewById(R.id.create_database); Button addData = (Button) findViewById(R.id.add_data); Button updateData = (Button) findViewById(R.id.update_data); Button deleteData = (Button) findViewById(R.id.delete_data); Button queryData = (Button) findViewById(R.id.query_data); createDatabase.setOnClickListener(this); addData.setOnClickListener(this); updateData.setOnClickListener(this); deleteData.setOnClickListener(this); queryData.setOnClickListener(this); } @Override public void onClick(View view) { Book book = null; switch (view.getId()){ case R.id.create_database: LitePal.getDatabase();//创建数据库 break; case R.id.add_data: book = new Book(); book.setName("The Da Vinci Code"); book.setAuthor("Dan Brown"); book.setPages(454); book.setPrice(16.96); book.setPress("Unknow"); book.save(); break; case R.id.update_data: book = new Book(); //设置要更新的数据 book.setPrice(14.95); book.setPress("Anchor"); book.updateAll("name = ? and author = ?","The Da Vinci Code","Dan Brown"); break; case R.id.delete_data: DataSupport.deleteAll(Book.class,"price < ?","15"); break; case R.id.query_data: List<Book> books = DataSupport.findAll(Book.class); for(Book b: books){ Log.d("MainActivity","book name is " + b.getName()); Log.d("MainActivity","book author is " + b.getAuthor()); Log.d("MainActivity","book pages is " + b.getPages()); Log.d("MainActivity","book price is " + b.getPrice()); Log.d("MainActivity","book press is " + b.getPress()); } break; default: break; } }}
- 数据存储
- 数据存储
- 数据存储
- 存储数据
- 存储数据
- 数据存储
- 数据存储
- 数据存储
- 数据存储
- 数据存储
- 数据存储
- 数据存储
- 数据存储
- 数据存储
- 数据存储
- 数据存储
- 数据存储
- 数据存储
- [记录] 算法学习集合
- C++11:基于范围的for循环、静态断言
- Linux下的静态库和动态库的使用和制作
- 欢迎使用CSDN-markdown编辑器
- Python的两种运行方式
- 数据存储
- html5学习之旅第一篇
- You Don't Need jQuery
- ocr-detection
- 虚拟机 Linux 安装tds (二)
- Nginx-一个IP配置多个站点
- android 获取当前屏幕显示的Activity
- Hdu 5396 Expression 区间DP+组合数
- 基本雷达测高工具箱BRAT(Basic Radar Altimetry Toolbox)的API