1. Socket通信の仕組みと実装例
Androidアプリがサーバーと通信する方法として、HTTPとSocketの2つがある。HTTPは「リクエスト→レスポンス」の一方向型であり、クライアントから要求があった時だけサーバーが応答を返す。一方、Socketはコネクション確立後、双方向に自由にデータを送受信できる。そのため、サーバー側からクライアントへ能動的にデータをプッシュする用途に適している。Socket(ソケット)はOSが提供する抽象レイヤで、アプリケーションがネットワークを介して他のアプリと通信するための出入口(ポート)を提供する。データ損失が少なく、実装もシンプルという特徴を持つ。
1.1 Socketの分類
TCP/IPプロトコルファミリには2種類の主要Socketタイプがある。
・ストリームソケット(TCP):信頼性の高いバイトストリームを提供。データ順序が保証され、再送制御を行う。
・データグラムソケット(UDP):パケット単位でデータを送信。コネクションレスで高速だが信頼性は低い。
1.2 サーバー側実装(Javaコンソール)
import java.io.*;
import java.net.*;
public class MyServer {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(555);
System.out.println("サーバー起動、クライアント接続を待機...");
Socket socket = serverSocket.accept();
DataInputStream input = new DataInputStream(socket.getInputStream());
DataOutputStream output = new DataOutputStream(socket.getOutputStream());
String msg = input.readUTF();
System.out.println("受信: " + msg);
output.writeUTF("Server: " + msg);
output.flush();
input.close();
socket.close();
serverSocket.close();
}
}
1.3 Androidクライアント実装
public class MainActivity extends Activity {
private Button sendBtn;
private TextView resultText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sendBtn = (Button) findViewById(R.id.button);
resultText = (TextView) findViewById(R.id.text);
sendBtn.setOnClickListener(v -> startSocketTask());
}
private void startSocketTask() {
new Thread(() -> {
try {
Socket socket = new Socket("10.20.90.3", 555);
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
DataInputStream dis = new DataInputStream(socket.getInputStream());
dos.writeUTF("Hello from Android");
String response = dis.readUTF();
runOnUiThread(() -> resultText.setText(response));
dos.close();
dis.close();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
このサンプルではAndroidクライアントがサーバーにメッセージを送信し、サーバーがそのメッセージにプレフィックスを付けて返す。実行前に必ずサーバーを先に起動しておく必要がある。
2. ListViewのスクロール追従によるページング読み込み
大量のデータを表示する際、すべてを一度に読み込むとメモリ負荷が大きい。そこでユーザーがリストの最下部までスクロールしたら、追加データを非同期で取得して末尾に挿入する方式を実装する。
2.1 メインレイアウト(activity_main.xml)
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/listview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
</RelativeLayout>
2.2 各アイテムのレイアウト(list_item.xml)
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#aa00ff">
<TextView
android:id="@+id/textview"
android:layout_width="match_parent"
android:layout_height="60dp"
android:textSize="18sp"
android:textColor="#aa0000"
android:singleLine="true" />
</RelativeLayout>
2.3 フッター(ローディング表示)レイアウト(footer_loading.xml)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="データ読み込み中..."
android:textSize="18sp"
android:textColor="#000000" />
</LinearLayout>
2.4 データ生成用クラス(DataProvider.java)
import java.util.ArrayList;
import java.util.List;
public class DataProvider {
public List<String> getPage(int pageNumber, int pageSize) {
List<String> items = new ArrayList<>();
int start = (pageNumber - 1) * pageSize + 1;
for (int i = 0; i < pageSize; i++) {
items.add("項目 " + (start + i));
}
return items;
}
}
2.5 メインActivity(ListViewPagingActivity.java)
public class ListViewPagingActivity extends Activity {
private ListView listView;
private ArrayAdapter<String> adapter;
private List<String> data = new ArrayList<>();
private View footerView;
private static final int PAGE_SIZE = 30;
private static final int MAX_PAGES = 5;
private int currentPage = 1;
private boolean isLoading = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listView = (ListView) findViewById(R.id.listview);
footerView = getLayoutInflater().inflate(R.layout.footer_loading, null);
// 最初のページをロード
loadPage(1);
adapter = new ArrayAdapter<>(this, R.layout.list_item, R.id.textview, data);
listView.addFooterView(footerView);
listView.setAdapter(adapter);
listView.removeFooterView(footerView);
listView.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {}
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if (totalItemCount == 0) return;
int lastVisible = firstVisibleItem + visibleItemCount - 1;
if (lastVisible == totalItemCount - 1 && !isLoading && currentPage < MAX_PAGES) {
isLoading = true;
listView.addFooterView(footerView);
new Thread(() -> {
try { Thread.sleep(2000); } catch (InterruptedException e) {}
final List<String> newData = new DataProvider().getPage(currentPage + 1, PAGE_SIZE);
runOnUiThread(() -> {
data.addAll(newData);
adapter.notifyDataSetChanged();
listView.removeFooterView(footerView);
currentPage++;
isLoading = false;
});
}).start();
}
}
});
}
private void loadPage(int page) {
data.addAll(new DataProvider().getPage(page, PAGE_SIZE));
}
}
この実装ではスクロールが最下部に達した時、新しいデータを非同期で取得し、フッターで「読み込み中」を表示する。下部に到達するたびにページ番号を進め、最大ページに達したらそれ以上読み込まない。
3. Spinner(ドロップダウンリスト)の基本とカスタマイズ
Spinnerは選択肢をドロップダウンまたはダイアログで表示するウィジェット。以下ではシステム標準スタイルとカスタムアダプタを使った例を示す。
3.1 レイアウト(activity_spinner.xml)
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Spinner サンプル"
android:textSize="25dp"
android:layout_gravity="center_horizontal" />
<!-- システム標準ドロップダウン -->
<Spinner
android:id="@+id/spinner_std_dropdown"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:entries="@array/cities"
android:spinnerMode="dropdown" />
<!-- システム標準ダイアログ -->
<Spinner
android:id="@+id/spinner_std_dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:entries="@array/cities"
android:spinnerMode="dialog" />
<!-- カスタムアダプタ(ドロップダウン) -->
<Spinner
android:id="@+id/spinner_custom_dropdown"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<!-- カスタムアダプタ(ダイアログ) -->
<Spinner
android:id="@+id/spinner_custom_dialog1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:spinnerMode="dialog" />
<Spinner
android:id="@+id/spinner_custom_dialog2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:spinnerMode="dialog" />
<Button
android:id="@+id/btn_next"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="次の画面へ" />
</LinearLayout>
</RelativeLayout>
3.2 res/values/strings.xml
<resources>
<string-array name="cities">
<item>選択してください</item>
<item>東京</item>
<item>大阪</item>
<item>名古屋</item>
</string-array>
</resources>
3.3 メインActivity(SpinnerActivity.java)
public class SpinnerActivity extends Activity {
private Spinner spinnerCustomDropdown;
private Spinner spinnerCustomDialog1;
private Spinner spinnerCustomDialog2;
private final String[] arrayData = {"項目A", "項目B", "項目C", "項目D"};
private final List<String> listData = Arrays.asList("選択肢1", "選択肢2", "選択肢3", "選択肢4");
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_spinner);
spinnerCustomDropdown = (Spinner) findViewById(R.id.spinner_custom_dropdown);
spinnerCustomDialog1 = (Spinner) findViewById(R.id.spinner_custom_dialog1);
spinnerCustomDialog2 = (Spinner) findViewById(R.id.spinner_custom_dialog2);
// カスタムアダプタ1(配列から)
BaseAdapter adapter1 = new BaseAdapter() {
@Override
public int getCount() { return arrayData.length; }
@Override
public Object getItem(int position) { return null; }
@Override
public long getItemId(int position) { return 0; }
@Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView tv = new TextView(SpinnerActivity.this);
tv.setText(arrayData[position]);
tv.setTextSize(18);
return tv;
}
};
spinnerCustomDropdown.setAdapter(adapter1);
// カスタムアダプタ2(リストから)
BaseAdapter adapter2 = new BaseAdapter() {
@Override
public int getCount() { return listData.size(); }
@Override
public Object getItem(int position) { return null; }
@Override
public long getItemId(int position) { return 0; }
@Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView tv = new TextView(SpinnerActivity.this);
tv.setText(listData.get(position));
tv.setTextColor(Color.BLUE);
tv.setTextSize(18);
return tv;
}
};
spinnerCustomDialog1.setAdapter(adapter2);
// システム標準ArrayAdapterの例
ArrayAdapter<String> adapterStd = new ArrayAdapter<>(
this, android.R.layout.simple_spinner_item, arrayData);
spinnerCustomDialog2.setAdapter(adapterStd);
// 選択イベント
spinnerCustomDialog1.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
Toast.makeText(SpinnerActivity.this,
"選択: " + listData.get(pos), Toast.LENGTH_SHORT).show();
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
});
findViewById(R.id.btn_next).setOnClickListener(v -> {
startActivity(new Intent(SpinnerActivity.this, NextActivity.class));
});
}
}
システム標準のandroid:entriesを使うとXMLだけで簡単に選択肢を設定できる。一方、BaseAdapterを継承して独自のViewを返すことで、フォントサイズや色などを自由にカスタマイズしたSpinnerを作成できる。選択時にOnItemSelectedListenerで処理を追加することも可能。