4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Java]ListViewを利用したコレクションのウィジェットを実装する

Last updated at Posted at 2021-04-13

今回の投稿では、ListViewを利用したコレクションのウィジェットの実装方法について書いていきます。
ウィジェットに対する投稿は比較的少なかったため、この投稿で一通りの説明をしてみたいと思います。

この投稿での完成イメージ

以下のスクリーンショットで示すように、ウィジェットの設定をすると設定アクティビティで設定を行なってからウィジェットを出すような実装となります。
ウィジェット.png

完成したものの挙動確認

これから説明する内容は、既にGooglePlayにて公開されているアプリで実装されているものについての解説となります。
実際の動作を確認したい場合には、以下のアプリで実装されていますので確認してみてください。

記録が残るToDoリスト 〜ウィジェット機能付きで無料でシンプルな操作のやること管理ツール〜
では解説を始めていきたいと思います。

1.AppWidgetProviderInfoメタデータを設定する

res/xmlフォルダを作成して配下にtodo_appwidget_info.xmlというファイルを作成します。
このメタデータの設定によって、設定アクティビティを利用するかどうか、ウィジェットのレイアウトや設定が決められます。
今回は以下のように設定します。

todo_appwidget_info.xml
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="180dp"
    android:minResizeWidth="110dp"
    android:minHeight="110dp"
    android:minResizeHeight="40dp"
    android:updatePeriodMillis="86400000"
    android:previewImage="@drawable/ic_widget_icon"
    android:initialLayout="@layout/todo_appwidget"
    android:configure="com.highcom.todolog.widget.ToDoAppWidgetConfigure"
    android:resizeMode="horizontal|vertical"
    android:widgetCategory="home_screen">
</appwidget-provider>

各要素について解説します。

  • minWidth、minResizeWidth、minHeight、minResizeHeight

これは、ウィジェットを配置時にminWidth、minHeightのサイズで配置されます。minResizeWidhth、minResizeHeightはサイズ変更できる最小サイズの指定となるので、配置時の初期サイズよりも小さくできるように設定する場合には、この要素を設定します。
サイズの指定については以下のようになっています。

セルの数 使用可能なサイズ(dp)
1 40 dp
2 110 dp
3 180 dp
4 250 dp
... ...
n 70 x n - 30 dp
  • updatePeriodMillis

これは、画面に配置された際の自動更新頻度をミリ秒で設定します。
電力の消費の関係からできるだけ低い頻度の更新となるような設定が推奨されています。

  • previewImage

これはウィジェットを配置する際のイメージ画像を設定します。
以下のようにウィジェットを選択する際のイメージ画像になります。

  • initial_layout

配置されるウィジェットのレイアウトを定義したxmlファイルを指定します。
このレイアウトファイルについては次の項目で説明します。

  • configure

設定アクティビティを利用する場合には、この要素を設定します。
冒頭のイメージ画像のようにタスクリスト選択のアクティビティを挟むように今回は設定しています。
この設定アクティビティについては次の項目で説明します。

  • resizeMode

配置したウィジェットのサイズ変更ができる方向を指定します。縦と横の両方を変更したい場合にはこのように設定します。

  • widgetCategory

ウィジェットが配置できる場所を指定します。ロック画面とホーム画面が選択できるようですが、最新のバージョンではホーム画面の設定のみが有効になっているようです。

2.設定アクティビティの実装をする

メタデータの要素でandroid:configureで定義した設定アクティビティについて実装します。

2.1.AndroidManifest.xmlに設定アクティビティを定義

以下のように設定アクティビティであることを宣言します。

AndroidManifest.xml
<activity android:name=".widget.ToDoAppWidgetConfigure">
	<intent-filter>
		<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
	</intent-filter>
</activity>

2.2.設定アクティビティのレイアウトを定義

設定アクティビティは、AndroidManifest.xmlの設定意外は普通のアクティビティと同じように設定できます。
なので、res/layoutの配下にレイアウトファイルを定義します。
タスクリストを選択する画面なので、以下のようにListViewを配置したレイアウトとします。

activity_to_do_appwidget_configure.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    tools:context=".widget.ToDoAppWidgetConfigure">

        <ListView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/app_widget_list_view"
            tools:ignore="MissingConstraints">
        </ListView>
</androidx.constraintlayout.widget.ConstraintLayout>

2.3.設定アクティビティの実装

以下のように通常のアクティビティのようにAppCompatActivityを継承した実装をします。
ViewModelProviderを利用してデータベースからデータを取得して、カスタムしたアダプタに設定する実装となっていますが、以降で要点について説明します。

ToDoAppWidgetConfigure.java
public class ToDoAppWidgetConfigure extends AppCompatActivity {

    public static final String SELECT_WIDGET_GROUP_ID = "selectWidgetGroupId";
    public static final String SELECT_WIDGET_GROUP_NAME = "selectWidgetGroupName";
    private int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_to_do_app_widget_configure);
        setTitle(getString(R.string.group_select));

        Intent intent = getIntent();
        Bundle extras = intent.getExtras();
        if (extras != null) {
            mAppWidgetId = extras.getInt(
                    AppWidgetManager.EXTRA_APPWIDGET_ID,
                    AppWidgetManager.INVALID_APPWIDGET_ID);
        }

        if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
            finish();
        }

        ListView listView = findViewById(R.id.app_widget_list_view);
        GroupViewModel groupViewModel = new ViewModelProvider(this).get(GroupViewModel.class);
        groupViewModel.getGroupList().observe(this, groupList -> {
            ArrayList<DrawerListItem> drawerListItem = new ArrayList<>();
            for (Group group : groupList) {
                drawerListItem.add(new DrawerListItem(group.getGroupName()));
            }
            DrawerListAdapter adapter = new DrawerListAdapter(this, R.layout.row_drawerlist, drawerListItem);
            listView.setAdapter(adapter);

            listView.setOnItemClickListener((adapterView, view, i, l) -> {
                long selectGroupId = groupList.get(i).getGroupId();
                String selectGroupName = groupList.get(i).getGroupName();
                saveSelectWidgetGroupPref(getApplicationContext(), mAppWidgetId, selectGroupId, selectGroupName);
                RemoteViews views = new RemoteViews(getPackageName(), R.layout.todo_appwidget);
                AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(getApplicationContext());
                appWidgetManager.updateAppWidget(mAppWidgetId, views);
                ToDoAppWidgetProvider.updateAppWidget(this.getApplicationContext(), appWidgetManager, mAppWidgetId);

                Intent resultValue = new Intent();
                resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
                setResult(RESULT_OK, resultValue);
                finish();
            });
        });
    }

    static void saveSelectWidgetGroupPref(Context context, int appWidgetId, long selectGroupId, String selectGroupName) {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        prefs.edit().putLong(SELECT_WIDGET_GROUP_ID + appWidgetId, selectGroupId).apply();
        prefs.edit().putString(SELECT_WIDGET_GROUP_NAME + appWidgetId, selectGroupName).apply();
    }

    static long loadSelectWidgetGroupIdPref(Context context, int appWidgetId) {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        long groupId = prefs.getLong(SELECT_WIDGET_GROUP_ID + appWidgetId, -1);
        return groupId;
    }

    static String loadSelectWidgetGroupNamePref(Context context, int appWidgetId) {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        String groupName = prefs.getString(SELECT_WIDGET_GROUP_NAME + appWidgetId, "");
        return groupName;
    }

    static void deleteSelectWidgetGroupPref(Context context, int appWidgetId) {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        prefs.edit().remove(SELECT_WIDGET_GROUP_ID + appWidgetId).apply();
        prefs.edit().remove(SELECT_WIDGET_GROUP_NAME + appWidgetId).apply();
    }
}

onCreateメソッドについて

このメソッドで注目しておきたいのが以下の部分。

ToDoAppWidgetConfigure.java
                saveSelectWidgetGroupPref(getApplicationContext(), mAppWidgetId, selectGroupId, selectGroupName);
                RemoteViews views = new RemoteViews(getPackageName(), R.layout.todo_appwidget);
                AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(getApplicationContext());
                appWidgetManager.updateAppWidget(mAppWidgetId, views);
                ToDoAppWidgetProvider.updateAppWidget(this.getApplicationContext(), appWidgetManager, mAppWidgetId);

saveSelectWidgetGroupPrefを呼び出して、設定アクティビティで選択したデータを保存した後に、ウィジェットであるToDoAppWidgetProviderクラスのupdateAppWidgetメソッドを呼び出しています。
これは、画面にウィジェットを配置すると、ウィジェットであるToDoAppWidgetProviderのonUpdateが先に呼び出されてから、設定アクティビティのタスクリスト選択となるため、選択した情報を改めて渡す必要があるため、ここでウィジェットをアップデートしています。

saveSelectWidgetGroupPrefメソッドについて

選択したデータをSharedPreferenceに保存しています。
ウィジェットには、保存せずともIntentでデータを渡す事はできますが、再起動した時などは保存したデータから読み出す必要があるためです。

loadSelectWidgetGroupIdPref, loadSelectWidgetGroupNamePrefメソッドについて

ウィジェットが配置された時にウィジェットから利用されることを想定したメソッドです。ウィジェット側ではSharedPreferenceで保存されたデータを利用して設定をおこないます。

deleteSelectWidgetGroupPrefメソッドについて

ウィジェットが削除される時にウィジェットから利用されることを想定したメソッドです。Key値にウィジェットIDを入れているため、ウィジェットが削除された場合、そのウィジェットに対応するIDのデータも削除しておく必要があるためです。

3.コレクションのウィジェットを実装する

コレクションのウィジェットを実装する場合、以下の3つのクラスを継承して実装を行う必要があります。

  • AppWidgetProvider

ウィジェットに対するブロードキャストを処理するクラスであり、RemoteViewsServiceを呼び出してコレクションのウィジェットを生成します。

  • RemoteViewsService

コレクションウィジェットを生成するためのRemoteViewsFactoryを呼び出すためのサービスクラスです。

  • RemoteViewsFactory

コレクションのウィジェットを生成する場合の本体となるクラス。ListViewを利用する場合のAdapterクラスと同じ位置付けのもの。

これらについて順番に説明していきます。

3.1.AndroidManifest.xmlにウィジェットの定義を追加

以下のように、AppWidgetProviderとRemoteViewsServiceを継承したクラスの定義を追加します。
なお、RemoteViewsFactoryを継承したクラスについては、RemoteViewsServiceのインナークラスになるため定義は不要です。

AndroidManifest.xml
        <receiver android:name=".widget.ToDoAppWidgetProvider">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>

            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/todo_appwidget_info" />
        </receiver>

        <service android:name=".widget.ToDoWidgetRemoteViewsService"
            android:permission="android.permission.BIND_REMOTEVIEWS"></service>

ToDoAppWidgetProviderのメタデータの要素で、1.で定義したtodo_appwidget_info.xmlを利用することを宣言しています。

3.2.ウィジェットのレイアウトファイルを定義する

ウィジェットのレイアウトとそのアイテムの定義を以下のように定義します。

todo_appwidget.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:background="@color/through_white"
    android:padding="@dimen/widget_margin"
    android:orientation="vertical">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="@dimen/widget_title_min_height"
            android:id="@+id/todo_widget_title_view"
            android:text="@string/widget_title"
            android:textColor="@color/white"
            android:background="@color/deepgreen"
            android:textSize="14dp"
            android:gravity="center"
            android:textAllCaps="true"/>
    </FrameLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <ListView
            android:id="@+id/todo_widget_list_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/offwhite"
            android:dividerHeight="1dp"
            android:divider="@android:color/darker_gray"
            tools:listitem="@layout/todo_widget_list_item" />
    </LinearLayout>
</LinearLayout>
todo_widget_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:paddingLeft="@dimen/widget_listview_padding_x"
    android:paddingRight="@dimen/widget_listview_padding_x"
    android:paddingStart="@dimen/widget_listview_padding_x"
    android:paddingEnd="@dimen/widget_listview_padding_x"
    android:minHeight="@dimen/widget_listview_item_height"
    android:weightSum="2"
    android:id="@+id/widget_item_container"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/widget_todo_contents"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:textColor="@color/black"
        android:textSize="14dp"
        android:layout_gravity="center_vertical"/>

</LinearLayout>

これらの定義は、通常のListViewの定義と同様です。

3.3.ToDoAppWigetProviderを実装する

以下のような実装になり、各メソッドの要点について説明します。

ToDoAppWidgetProvider.java
public class ToDoAppWidgetProvider extends AppWidgetProvider {
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        final int N = appWidgetIds.length;

        // Perform this loop procedure for each App Widget that belongs to this provider
        for (int i=0; i<N; i++) {
            int appWidgetId = appWidgetIds[i];

            updateAppWidget(context, appWidgetManager, appWidgetId);
        }
    }

    static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
        String selectGroupName = ToDoAppWidgetConfigure.loadSelectWidgetGroupNamePref(context, appWidgetId);
        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.todo_appwidget);

        Intent titleIntent = new Intent(context, ToDoMainActivity.class);
        titleIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        views.setTextViewText(R.id.todo_widget_title_view, selectGroupName);
        PendingIntent titlePendingIntent = PendingIntent.getActivity(context, 0, titleIntent, 0);
        // タイトルを押下した時のアクションを定義する
        views.setOnClickPendingIntent(R.id.todo_widget_title_view, titlePendingIntent);

        Intent listIntent = new Intent(context, ToDoWidgetRemoteViewsService.class);
        listIntent.setData(Uri.fromParts("content", Integer.toString(appWidgetId), null));
        views.setRemoteAdapter(R.id.todo_widget_list_view, listIntent);
        // リストを選択した時のアクションを定義する
        Intent clickIntentTemplate = new Intent(context, ToDoMainActivity.class);
        clickIntentTemplate.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent clickPendingIntentTemplate = TaskStackBuilder.create(context)
                .addNextIntentWithParentStack(clickIntentTemplate)
                .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
        views.setPendingIntentTemplate(R.id.todo_widget_list_view, clickPendingIntentTemplate);

        appWidgetManager.updateAppWidget(appWidgetId, views);
    }

    public static void sendRefreshBroadcast(Context context) {
        Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
        intent.setComponent(new ComponentName(context, ToDoAppWidgetProvider.class));
        context.sendBroadcast(intent);
    }

    @Override
    public void onReceive(final Context context, Intent intent) {
        final String action = intent.getAction();
        if (action.equals(AppWidgetManager.ACTION_APPWIDGET_UPDATE)) {
            // refresh all your widgets
            AppWidgetManager mgr = AppWidgetManager.getInstance(context);
            ComponentName cn = new ComponentName(context, ToDoAppWidgetProvider.class);
            mgr.notifyAppWidgetViewDataChanged(mgr.getAppWidgetIds(cn), R.id.todo_widget_list_view);
        }
        super.onReceive(context, intent);
    }
}

onUpdateメソッドについて

ウィジェットを配置時には、appWidgetIdsには、配置しようとしているウィジェットのIDが1つだけ入ります。複数のウィジェットを配置した状態で、再起動した場合などにappWidgetIdsは複数入ってきます。ウィジェットを画面に配置した時点でこのメソッドが呼び出されるため、設定アクティビティで選択したデータはこのメソッドが呼び出されるタイミングでは決まっていない状態となります。

updateAppWidgetメソッドについて

このメソッドは、onUpdateからと設定アクティビティで選択した際に明示的に呼び出されています。
設定アクティビティから明示的に呼び出しているのは、選択したデータをSharedPreferenceに保存してから改めてウィジェットを更新するためです。
onUpdateの呼び出しは、再起動時に保存されたSharedPreferenceからデータを読み出して呼び出されます。
また、注目すべき実装ポイントは以下。

ToDoAppWidgetProvider.java
        Intent listIntent = new Intent(context, ToDoWidgetRemoteViewsService.class);
        listIntent.setData(Uri.fromParts("content", Integer.toString(appWidgetId), null));
        views.setRemoteAdapter(R.id.todo_widget_list_view, listIntent);
        // リストを選択した時のアクションを定義する
        Intent clickIntentTemplate = new Intent(context, ToDoMainActivity.class);
        clickIntentTemplate.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent clickPendingIntentTemplate = TaskStackBuilder.create(context)
                .addNextIntentWithParentStack(clickIntentTemplate)
                .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
        views.setPendingIntentTemplate(R.id.todo_widget_list_view, clickPendingIntentTemplate);

ToDoWidgetRemoteViewsServiceクラスを利用してコレクションのウィジェットを生成しています。
ウィジェットID毎に処理を行いたいためToDoWidgetRemoteViewsFactoryにウィジェットIDをUriを使って渡しています。
なお、putExtraを利用すると、うまくFactoryが生成されないので注意して下さい。
また、コレクションが選択された際にアプリのメイン画面を起動するように実装をしています。addFlagsをしているのは、アプリのメイン画面が多重で起動されることを防ぐために設定しています。

sendRefreshBroadcastメソッドについて

外側のアプリからウィジェットに対してイベントを発行するためのメソッドです。

onReceiveメソッドについて

ウィジェット自身がイベントを受信する事ができるようにするためのメソッドです。

3.4.ToDoAppWidgetServiceを実装する

このクラスは、RemoteViewsFactoryを生成するためのonGetViewsFactoryメソッドをオーバーライドするだけです。

ToDoWidgetRemoteViewsService.java
public class ToDoWidgetRemoteViewsService extends RemoteViewsService {
    @Override
    public RemoteViewsFactory onGetViewFactory(Intent intent) {
        return new ToDoWidgetRemoteViewsFactory(this.getApplicationContext(), intent);
    }
}

3.5.ToDoWidgetRemoteViewsFactoryを実装する

コレクションのウィジェットを実装する上でのメインとなるクラスです。
実装は以下のようになっており、要点を説明していきます。

ToDoWidgetRemoteViewsFactory.java
public class ToDoWidgetRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
    private Context mContext;
    private int mAppWidgetId;
    private List<ToDoAndLog> mTodoAndLogList;
    private static final int NUMBER_OF_THREADS = 4;
    static final ExecutorService databaseWriteExtractor = Executors.newFixedThreadPool(NUMBER_OF_THREADS);

    public ToDoWidgetRemoteViewsFactory(Context applicationContext, Intent intent) {
        mContext = applicationContext;
        if (intent.getData() != null) {
            mAppWidgetId = Integer.parseInt(intent.getData().getSchemeSpecificPart());
        }
    }

    @Override
    public void onCreate() {
    }

    @Override
    public void onDataSetChanged() {
        final long identityToken = Binder.clearCallingIdentity();
        long selectGroupId = ToDoAppWidgetConfigure.loadSelectWidgetGroupIdPref(mContext, mAppWidgetId);

        List<Future<?>> futureList = new ArrayList<>();
        // ワーカースレッドで実行する。
        Future<?> future = databaseWriteExtractor.submit(() -> {
            mTodoAndLogList = ToDoLogRepository.getInstance(mContext).getTodoListOnlyToDoByTaskGroupSync(selectGroupId);
        });
        futureList .add(future);
        // ワーカースレッドその処理完了を待つ
        for (Future<?> f : futureList) {
            try {
                f.get();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        futureList.clear();
        Binder.restoreCallingIdentity(identityToken);
    }

    @Override
    public void onDestroy() {
        ToDoAppWidgetConfigure.deleteSelectWidgetGroupPref(mContext, mAppWidgetId);
    }

    @Override
    public int getCount() {
        return mTodoAndLogList.size();
    }

    @Override
    public RemoteViews getViewAt(int i) {
        if (i == AdapterView.INVALID_POSITION) {
            return  null;
        }

        RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.todo_widget_list_item);
        rv.setTextViewText(R.id.widget_todo_contents, mTodoAndLogList.get(i).toDo.getContents());

        Intent fillInIntent = new Intent();
        fillInIntent.putExtra("TASK_TEXT", mTodoAndLogList.get(i).toDo.getContents());
        rv.setOnClickFillInIntent(R.id.widget_item_container, fillInIntent);

        return rv;
    }

    @Override
    public RemoteViews getLoadingView() {
        return null;
    }

    @Override
    public int getViewTypeCount() {
        return 1;
    }

    @Override
    public long getItemId(int i) {
        return 0;
    }

    @Override
    public boolean hasStableIds() {
        return false;
    }
}

ToDoWidgetRemoteViewsFactoryコンストラクタについて

ToDoAppWidgetProviderからUriで渡されたデータであるウィジェットIDをここで取得しています。

onDataSetChangedメソッドについて

データに変更がある毎に呼び出されるメソッドです。なので、ここで基本的にはデータを読み出す処理を実装します。

onDestroyメソッドについて

ウィジェットが削除される場合に呼びださます。なので、ここでの実装は対応するウィジェットIDのSharedPreferenceのデータを削除するための処理を実装しています。

getCountメソッドについて

コレクションであるListViewに表示する数を返却するように実装しています。

getViewAtメソッドについて

各セル単位で呼び出されるので、引数で渡されている要素の順番を利用してデータを設定していきます。

まとめ

以上の説明で、ListViewを利用したコレクションのウィジェットの実装ができました。
ウィジェットが関連するコードについては一通り掲載しましたが、説明についてはウィジェットに関連する部分について中心に説明していきました。
そのため、説明が不足している部分などあるかと思いますが、分かりづらい点がありましたら是非ご意見下さい。

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?