ContentProviderは、アプリがデータを他のアプリと共有することを支援します。もし私のアプリでContentProviderを実装して提供する場合は、他のアプリは、ContentResolverを通し、私のアプリに実装されたContentProviderにアクセスすることができます。 ContentProviderはデータベースと同様にquery、insert、update、またはdeleteなどのAPIを提供しています。
他のアプリは、ContentProviderが提供するAPIを利用してデータを読み取り、保存、削除することができます。 もちろん、内部的にSQLiteなどのデータベースを使用してデータを管理する必要があります。 ContentProviderは、他のアプリとデータを共有するためのインタフェースと考えることができます。
ContentProvider宣言とアクセス制限
ContentProviderは AndroidManifest.xml
に次のように定義することができます。
android:authorities
属性はContentProviderのIDと同じです。他のアプリで私のアプリのProviderを検索するときauthorityを知っている必要があります。
<provider
android:name=".SampleContentProvider"
android:authorities="com.example.contentprovidersample.provider"
android:exported="true"
android:permission="com.example.contentprovidersample.provider.READ_WRITE"/>
android:permission
属性を定義しないと、私のエプマンContentProviderにアクセスすることができます。
この属性にパーミッションを設定すると、このパーミッションを持っているエプマン内アプリのContentProviderにアクセスすることができます。
ContentProviderアプローチ
ContentProviderはデータベースと同様にquery、insert、update、またはdeleteなどのAPIを提供しています。 アプリは、Providerのauthorityを知っていればContentProviderを使用してProviderにアクセスすることができます。
以下は、AppでProviderにデータをQueryするコードです。まず、authorityでUriを作成し、このUriをContentResolverの引数として渡してqueryすることができます。 APIの使用方法は、SQLiteなどのデータベースと似ています。 projection、selectionなどを設定して、必要なデータを見つけることができます。そして返されるCursorオブジェクトを介してデータを読み取ることができます。
companion object {
const val TABLE_NAME = "cheeses"
const val AUTHORITY = "com.example.contentprovidersample.provider"
val URI_CHEESE: Uri = Uri.parse(
"content://" + AUTHORITY + "/" + TABLE_NAME)
)
val cursor = contentResolver.query(
SampleContentProvider.URI_CHEESE, // uri
null, // projection
null, // selection
null, // selectionArgs
Cheese.COLUMN_NAME // sortOrder
)
Uriは content://
というschemeにProviderのauthorityとtableの名前を組み合わせて作成します。
// 1
val URI_CHEESE_DIR: Uri =
Uri.parse("content://com.example.contentprovidersample.provider/cheeses")
// 2
val URI_CHEESE_ITEM: Uri =
Uri.parse("content://com.example.contentprovidersample.provider/cheeses/5")
上記のUriで // 1
はテーブル全体を指すUri。
// 2
は、最後にアイテムのIDを意味するIntegerがあります。
これはID 5の値を持っているアイテムを指すUri。
Uriによってアイテムに対してどのようなアクションを実行するかを決定することができます。
非同期的にアクセス
ActivityやFragmentでContentProviderに非同期的にアクセスするためにLoaderManager、CursorLoaderを使用することができます。 LoaderManagerはProviderのデータをすべて取得するとLoaderCallbacksを通じてCallbackをしてくれます。 また、追加、削除、更新、などでProviderのデータ変更があったときもCallbackをしてくれます。
ContentProviderのデータ管理
ContentProviderは私のアプリのデータを外部のエプドゥルと接続するインターフェイスです。 実際のデータを保存して管理していません。したがって、ContentProviderは内部的にデータベースを使用してデータを管理する必要があります。 データベースはSQLite、MongoDBなどを使用することができます。また、Android ArchitectureのRoomを利用して実装することもできます。
次は、ContentProviderの query()
メソッドです。内部的にSQLiteを使用してデータを管理しています。 (SampleDatabaseはSQLiteを抽象化したクラスです。コードは、次に紹介します。)
class SampleContentProvider : ContentProvider() {
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
val queryBuilder = SQLiteQueryBuilder()
queryBuilder.tables = Cheese.TABLE_NAME
val db = SampleDatabase.getInstance(context!!)
val cursor = queryBuilder.query(
db, projection, selection, selectionArgs, null, null, sortOrder)
ContentProviderサンプルとチュートリアル
今からチュートリアル方式でContentProviderを実装する簡単なアプリを作ってみましょう。
この記事で実装するSampleは、GitHub - ContentProvider Sampleで確認することができます。
プロジェクトの作成
ContentProviderクラスの実装
ContentProviderを継承するProviderクラスを作成します。 基本的には次のメソッドをオーバーライドする必要があります。それぞれのメソッドは、SQLiteを使用して、データを処理します。
- onCreate()
- query()
- insert()
- update()
- delete()
- getTypte()
SampleContentProvider.kt
ファイルを作成し、次のように実装します。
ここSampleDatabaseというクラスは、SQLiteを抽象化したクラスです。実装については、次の紹介します。
class SampleContentProvider : ContentProvider() {
companion object {
const val AUTHORITY = "com.example.contentprovidersample.provider"
val URI_CHEESE: Uri = Uri.parse(
"content://" + AUTHORITY + "/" + Cheese.TABLE_NAME)
const val CODE_CHEESE_DIR = 1
const val CODE_CHEESE_ITEM = 2
}
private var uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
init {
uriMatcher.addURI(AUTHORITY, Cheese.TABLE_NAME, CODE_CHEESE_DIR)
uriMatcher.addURI(AUTHORITY, "${Cheese.TABLE_NAME}/#", CODE_CHEESE_ITEM)
}
override fun onCreate(): Boolean {
return true
}
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
val code: Int = uriMatcher.match(uri)
return when (uriMatcher.match(uri)) {
CODE_CHEESE_DIR, CODE_CHEESE_ITEM -> {
val queryBuilder = SQLiteQueryBuilder()
queryBuilder.tables = Cheese.TABLE_NAME
val db = SampleDatabase.getInstance(context!!)
val cursor = queryBuilder.query(
db, projection, selection, selectionArgs, null, null, sortOrder)
cursor.setNotificationUri(context!!.contentResolver, uri)
cursor
}
else -> {
throw java.lang.IllegalArgumentException("Unknown URI: $uri")
}
}
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
return when (uriMatcher.match(uri)) {
CODE_CHEESE_DIR -> {
val id = SampleDatabase.getInstance(context!!)
.insert(Cheese.TABLE_NAME, null, values)
val insertedUri = ContentUris.withAppendedId(uri, id)
context!!.contentResolver.notifyChange(insertedUri, null)
insertedUri
}
CODE_CHEESE_ITEM -> {
throw java.lang.IllegalArgumentException("Invalid URI, cannot insert with ID: $uri")
}
else -> {
throw java.lang.IllegalArgumentException("Unknown URI: $uri")
}
}
}
override fun update(uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<out String>?): Int {
return when (uriMatcher.match(uri)) {
CODE_CHEESE_DIR -> {
throw java.lang.IllegalArgumentException("Invalid URI, cannot update without ID$uri")
}
CODE_CHEESE_ITEM -> {
val id = ContentUris.parseId(uri)
val count = SampleDatabase.getInstance(context!!)
.update(Cheese.TABLE_NAME, values, "${Cheese.COLUMN_ID} = ?",
arrayOf(id.toString()))
context!!.contentResolver.notifyChange(uri, null)
count
}
else -> {
throw java.lang.IllegalArgumentException("Unknown URI: $uri")
}
}
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
return when (uriMatcher.match(uri)) {
CODE_CHEESE_DIR -> {
throw java.lang.IllegalArgumentException("Invalid URI, cannot update without ID: $uri")
}
CODE_CHEESE_ITEM -> {
val id = ContentUris.parseId(uri)
val count = SampleDatabase.getInstance(context!!)
.delete(Cheese.TABLE_NAME,
"${Cheese.COLUMN_ID} = ?",
arrayOf(id.toString()))
context!!.contentResolver.notifyChange(uri, null)
count
}
else -> {
throw java.lang.IllegalArgumentException("Unknown URI: $uri")
}
}
}
override fun getType(uri: Uri): String? {
return when (uriMatcher.match(uri)) {
CODE_CHEESE_DIR -> "vnd.android.cursor.dir/$AUTHORITY.$Cheese.TABLE_NAME"
CODE_CHEESE_ITEM -> "vnd.android.cursor.item/$AUTHORITY.$Cheese.TABLE_NAME"
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
}
}
Constantsは Cheese.kt
ファイルを作成し、ここに定義しました。
class Cheese {
companion object {
const val TABLE_NAME = "cheeses"
const val COLUMN_ID = BaseColumns._ID
const val COLUMN_NAME = "name"
val CHEESES = arrayOf(
"Abbaye de Belloc", "Abbaye du Mont des Cats", "Abertam", "Abondance", "Ackawi",
"Babybel", "Baguette Laonnaise", "Bakers", "Baladi", "Balaton", "Bandal", "Banon"
)
}
}
上記のコードで重要な部分をそれぞれ説明します。
UriMatcher
次のコードは、UriMatcherを定義するコードです。 UriMatcherはUriが一致するか否かを判断するUtilityクラスです。
UriMatcher.addURI()
は比較したいUriを登録するメソッドです。
最後の引数は、 UriMatcher.match()
の結果、引数として渡されたUriと一致したときに戻されるIntegerです。
もし一致するUriがない場合は-1を返します。
const val TABLE_NAME = "cheeses"
const val CODE_CHEESE_DIR = 1
const val CODE_CHEESE_ITEM = 2
private var uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
init {
uriMatcher.addURI(AUTHORITY, TABLE_NAME, CODE_CHEESE_DIR)
uriMatcher.addURI(AUTHORITY, "${TABLE_NAME}/#", CODE_CHEESE_ITEM)
}
例えば上で定義しUriMatcherを利用した時、次のコードのUriの match()
は CODE_CHEESE_DIR
を返します。
val uri = Uri.parse("content://com.example.contentprovidersample.provider/cheeses")
uriMatcher.match(uri)
他の例として、次のコードのUriの match()
は CODE_CHEESE_ITEM
を返します。
val uri = Uri.parse("content://com.example.contentprovidersample.provider/cheeses/5")
uriMatcher.match(uri)
UriMatcherの詳細については、Android Developer - Content uri patternsページをご確認ください。
UriMatcherを使用する理由
UriMatcherは次のように使用することができます。次のコードでUriがテーブル全体を意味する ..../cheeses
形で入ってきた場合、insertを実行して、
..../cheeses/5
よう、特定のIDが入力された形でUriが引数として渡された場合、Exceptionを発生させるように実装することができます。
override fun insert(uri: Uri, values: ContentValues?): Uri? {
return when (uriMatcher.match(uri)) {
CODE_CHEESE_DIR -> {
val id = SampleDatabase.getInstance(context!!)
.insert(Cheese.TABLE_NAME, null, values)
val insertedUri = ContentUris.withAppendedId(uri, id)
context!!.contentResolver.notifyChange(insertedUri, null)
insertedUri
}
CODE_CHEESE_ITEM -> {
throw java.lang.IllegalArgumentException("Invalid URI, cannot insert with ID: $uri")
}
else -> {
throw java.lang.IllegalArgumentException("Unknown URI: $uri")
}
}
}
ManifestにContentProvider定義
ContentProviderを実装したら、AppのAndroidManifestに定義する必要があります。
外部アプリからこのProviderへのアクセスを必要がある場合パーミッション属性を定義する必要があります。パーミッションを持っているエプマンアクセスすることができるからです。
<permission android:name="com.example.contentprovidersample.provider.READ_WRITE"/>
<application
<provider
android:name=".SampleContentProvider"
android:authorities="com.example.contentprovidersample.provider"
android:exported="true"
android:permission="com.example.contentprovidersample.provider.READ_WRITE"/>
</application>
上記のManifestでパーミッションを定義したがね。このパーミッションのProtectionLevelはnormalです。つまり、誰でも
<uses-permission>
でそのパーミッションを使用すると定義すると、得ることができます。 Providerは、常に開いていないが、私Providerが要求するパーミッションを簡単に知ることができますので、必要な場合パーミッションを設定して、アクセスすることができます。もしアプリと同じsignatureに構築されたエプマン権限を与えたい場合はProtectionLevelをsignatureに設定ください。
データベース(SQLite)の実装
このSampleでデータベースはSQLiteを使用しました。データベースの実装は、 SampleDatabase
クラスに抽象化しました。
SampleDatabase.kt
ファイルを作成し、次のように実装します。
class SampleDatabase {
companion object {
private const val DATABASE_NAME = "cheese.db"
private const val DATABASE_VERSION = 1
private var sInstance: DatabaseHelper? = null
@Synchronized
fun getInstance(context: Context): SQLiteDatabase {
if (sInstance == null) {
sInstance = DatabaseHelper(context, DATABASE_NAME, null, DATABASE_VERSION)
}
return sInstance!!.writableDatabase
}
class DatabaseHelper(
context: Context?,
name: String?,
factory: SQLiteDatabase.CursorFactory?,
version: Int) : SQLiteOpenHelper(context, name, factory, version) {
override fun onCreate(_db: SQLiteDatabase?) {
_db?.execSQL("CREATE TABLE ${Cheese.TABLE_NAME}"
+ " (${Cheese.COLUMN_ID} INTEGER PRIMARY KEY AUTOINCREMENT, "
+ " ${Cheese.COLUMN_NAME} TEXT NOT NULL);")
}
override fun onUpgrade(_db: SQLiteDatabase?, _oldVersion: Int, _newVersion: Int) {
_db?.execSQL("DROP TABLE IF EXISTS ${Cheese.TABLE_NAME}")
onCreate(_db)
}
}
}
}
コードを見れば SQLiteDatabase
とSQLiteOpenHelper
などを利用して実装しました。
Singleton実装したので、 SampleDatabase.getInstance()
でオブジェクトを取得することができます。
ContentProviderを紹介する文であるため、SQLiteのは詳しく説明しませんでした。
Activityの実装
今ContentProvider、Databaseをすべて実装しました。今私のアプリでは、Providerにアクセスするコードを実装することです。
MainActivity.kt
のlayoutあるactivity_main.xml
を次のように修正してくれます。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<LinearLayout
android:id="@+id/buttonLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="5dp">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Add"
android:onClick="addItem">
</Button>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Update"
android:onClick="updateItem">
</Button>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Remove"
android:onClick="removeItem">
</Button>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:scrollbars="vertical"/>
</LinearLayout>
MainActivity.kt
は次のように変更します。
class MainActivity : AppCompatActivity() {
companion object {
const val TAG = "MainActivity"
const val LOADER_CHEESES = 1
}
private var cheeseAdapter: CheeseAdapter = CheeseAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
populateInitialDataIfNeeded()
val list = findViewById<RecyclerView>(R.id.list)
list.layoutManager = LinearLayoutManager(list.context)
list.adapter = cheeseAdapter
LoaderManager.getInstance(this)
.initLoader(`LOADER_CHEESES`, null, loaderCallbacks)
}
private val loaderCallbacks: LoaderManager.LoaderCallbacks<Cursor> =
object : LoaderManager.LoaderCallbacks<Cursor> {
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor?> {
return CursorLoader(
applicationContext,
SampleContentProvider.URI_CHEESE, // uri
arrayOf<String>(Cheese.COLUMN_NAME), // projection
null, // selection
null, // selectionArgs
Cheese.COLUMN_NAME // sortOrder
)
}
override fun onLoadFinished(loader: Loader<Cursor?>, data: Cursor?) {
cheeseAdapter.setCheeses(data)
}
override fun onLoaderReset(loader: Loader<Cursor?>) {
cheeseAdapter.setCheeses(null)
}
}
internal class CheeseAdapter : RecyclerView.Adapter<CheeseAdapter.ViewHolder?>() {
private var cursor: Cursor? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(parent)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
if (cursor!!.moveToPosition(position)) {
holder.text.text = cursor!!.getString(
cursor!!.getColumnIndexOrThrow(Cheese.COLUMN_NAME)
)
}
}
fun setCheeses(cursor: Cursor?) {
this.cursor = cursor
notifyDataSetChanged()
}
internal class ViewHolder(parent: ViewGroup) :
RecyclerView.ViewHolder(
LayoutInflater.from(parent.context).inflate(
android.R.layout.simple_list_item_1, parent, false
)
) {
val text: TextView = itemView.findViewById(android.R.id.text1)
}
override fun getItemCount(): Int {
return if (cursor == null) {
0
} else {
cursor!!.count
}
}
}
private fun populateInitialDataIfNeeded() {
val cursor = contentResolver.query(
SampleContentProvider.URI_CHEESE,
null,
null,
null,
null
)
if (cursor != null && cursor.count == 0) {
Log.d(TAG, "Add initial data")
for (cheese in Cheese.CHEESES) {
val values = ContentValues()
values.put(Cheese.COLUMN_NAME, cheese)
contentResolver.insert(SampleContentProvider.URI_CHEESE, values)
}
}
}
fun addItem(view: View) {
val values = ContentValues()
values.put(Cheese.COLUMN_NAME, "New Item")
val uri = contentResolver.insert(SampleContentProvider.URI_CHEESE, values)
Log.d(TAG, "Added item: $uri")
}
fun updateItem(view: View) {
val uri = queryAndGetOne()
if (uri != null) {
Log.d(TAG, "Update item: $uri")
val values = ContentValues()
values.put(Cheese.COLUMN_NAME, "Updated Item")
contentResolver.update(uri, values, null, null)
}
}
fun removeItem(view: View) {
val uri = queryAndGetOne()
if (uri != null) {
Log.d(TAG, "Remove item: $uri")
contentResolver.delete(
uri,
null,
null
)
}
}
private fun queryAndGetOne() : Uri? {
val cursor = contentResolver.query(
SampleContentProvider.URI_CHEESE, // uri
null, // projection
null, // selection
null, // selectionArgs
Cheese.COLUMN_NAME // sortOrder
)
return if (cursor != null && cursor.count != 0) {
cursor.moveToFirst()
val id = cursor.getString(cursor.getColumnIndex(Cheese.COLUMN_ID))
val name = cursor.getString(cursor.getColumnIndex(Cheese.COLUMN_NAME));
val uri = ContentUris.withAppendedId(SampleContentProvider.URI_CHEESE, id.toLong())
Log.d(TAG, "query and return uri: $uri (id: $id, name: $name)")
uri
} else {
null
}
}
}
上記のコードでRecyclerViewを使用するため、アプリの build.gradle
の依存性にRecyclerViewライブラリを追加する必要があります。
dependencies {
...
implementation 'androidx.recyclerview:recyclerview:1.1.0'
}
LoaderManager, CursorLoader
LoaderManagerは非同期的にContentProviderのデータのインポートに役立つクラスです。 LoaderManagerに多数のLoaderを登録することができ、CursorLoaderを利用して、ContentProviderのデータをインポートすることができます。
次のコードで onCreateLoader()
はCursorLoaderを定義します。 CursorLoaderはどのプロバイダを読むかUri情報とqueryに必要なselectionなどの情報を引数として渡します。
queryが完了すると、 onLoadFinished()
でデータが渡されます。 Cursorからデータを読み取るRecyclerViewにアイテムが見えるように実装するされます。
companion object {
const val LOADER_CHEESES = 1
}
override fun onCreate(savedInstanceState: Bundle?) {
...
LoaderManager.getInstance(this)
.initLoader(`LOADER_CHEESES`, null, loaderCallbacks)
}
private val loaderCallbacks: LoaderManager.LoaderCallbacks<Cursor> =
object : LoaderManager.LoaderCallbacks<Cursor> {
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor?> {
return CursorLoader(
applicationContext,
SampleContentProvider.URI_CHEESE, // uri
arrayOf<String>(Cheese.COLUMN_NAME), // projection
null, // selection
null, // selectionArgs
Cheese.COLUMN_NAME // sortOrder
)
}
override fun onLoadFinished(loader: Loader<Cursor?>, data: Cursor?) {
cheeseAdapter.setCheeses(data)
}
override fun onLoaderReset(loader: Loader<Cursor?>) {
cheeseAdapter.setCheeses(null)
}
}
初期値の追加
アプリを起動し、データベースにデータがないため、動作を確認するのに良くはありません。 次のようなコードで、データがない場合にdummyデータを追加するようにしました。
private fun populateInitialDataIfNeeded() {
val cursor = contentResolver.query(
SampleContentProvider.URI_CHEESE,
null,
null,
null,
null
)
if (cursor != null && cursor.count == 0) {
Log.d(TAG, "Add initial data")
for (cheese in Cheese.CHEESES) {
val values = ContentValues()
values.put(Cheese.COLUMN_NAME, cheese)
contentResolver.insert(SampleContentProvider.URI_CHEESE, values)
}
}
}
insert(), update(), remove()
それぞれのボタンが押されると、insert、update、またはremoveを呼び出すコードを実装しました。
fun addItem(view: View) {
val values = ContentValues()
values.put(Cheese.COLUMN_NAME, "New Item")
val uri = contentResolver.insert(SampleContentProvider.URI_CHEESE, values)
Log.d(TAG, "Added item: $uri")
}
fun updateItem(view: View) {
val uri = queryAndGetOne()
if (uri != null) {
Log.d(TAG, "Update item: $uri")
val values = ContentValues()
values.put(Cheese.COLUMN_NAME, "Updated Item")
contentResolver.update(uri, values, null, null)
}
}
fun removeItem(view: View) {
val uri = queryAndGetOne()
if (uri != null) {
Log.d(TAG, "Remove item: $uri")
contentResolver.delete(
uri,
null,
null
)
}
}
updateとremoveは、すでに保存されたアイテムに対して実行を説得する。
次のように COLUMN_NAME
columnに配置されたデータの中で最も最初のアイテムを使用するように実装しました。
private fun queryAndGetOne() : Uri? {
val cursor = contentResolver.query(
SampleContentProvider.URI_CHEESE, // uri
null, // projection
null, // selection
null, // selectionArgs
Cheese.COLUMN_NAME // sortOrder
)
return if (cursor != null && cursor.count != 0) {
cursor.moveToFirst()
val id = cursor.getString(cursor.getColumnIndex(Cheese.COLUMN_ID))
val name = cursor.getString(cursor.getColumnIndex(Cheese.COLUMN_NAME));
val uri = ContentUris.withAppendedId(SampleContentProvider.URI_CHEESE, id.toLong())
Log.d(TAG, "query and return uri: $uri (id: $id, name: $name)")
uri
} else {
null
}
}
まとめ
Room with Content Providers Sampleを参照してContentProviderを簡単に実装してみました。参考にしたSampleはRoomを使用して、Javaで実装されていますが、これをSQLiteに変更し、kotlinに実装しました。
この記事で紹介されたSampleは、GitHub - ContentProvider Sampleで確認することができます。
参考
Related Posts
- エラー解決:android gradle plugin requires java 11 to run. you are currently using java 1.8.
- Android - コルーチンとRetrofitによる非同期通信の例
- Android - コルーチンでURL画像を読み込む
- Android - 振動、Vibrator、VibrationEffectの例
- Some problems were found with the configuration of task
- Query method parameters should either be a type that can be converted into a database column or a List
- UbuntuでAndroid 12オープンソースをダウンロードしてビルド
- Android - ViewModelを生成する方法
- Android - Transformations.map(), switchMap() の違い
- Android-Transformations.distinctUntilChanged()소개
- Android - TabLayoutの実装方法(+ ViewPager2)
- Android - 携帯電話の電話番号を取得する方法
- Android 12 - Splash Screens
- Android 12 - インクリメンタルインストール
- Android - adbコマンドでbugreportログファイルの抽出
- Android - adbコマンドでAppデータを削除する
- Android - adbコマンドでアプリ無効化、有効化
- Android - adbコマンドで特定のパッケージのPIDを検索
- Android - adbコマンドでパーミッションGrantまたはRevoke
- Android - adbコマンドで特定のパッケージのプロセスの終了
- Android - adbコマンドでapkのインストール、削除、
- Android - adb push、pullでファイルのコピー、ダウンロード
- Android - adbコマンドでscreen capture保存
- Android - adbコマンドでSystemアプリの削除、インストール
- Android - adbコマンドでsettings value確認、変更、
- Android 12 - IntentFilterのexported明示的な宣言
- Android - adbコマンドで工場出荷時の(Factory reset)
- Android - adb logcatコマンドでログ出力
- Android - adbコマンドでメモリダンプ(dump-heap)
- Android - adbコマンドでApp強制終了(force-stop)
- Android - adbコマンドでServiceの実行、終了
- Android - adbコマンドでActivity実行
- Android - adbコマンドでBroadcast配信
- Android - PackageManagerにPackage情報を取得する
- Android - ACTION_BOOT_COMPLETEDイベント受信