- アーキテクト(バックエンド)
- プロダクトマネージャー
- カスタマーサポートMFK
- 他92件の職種
-
開発
- アーキテクト(バックエンド)
- Railsエンジニア
- アーキテクト(フロントエンド)
- Webエンジニア
- エンジニアリングマネージャー
- バックエンドエンジニア/MFK
- フルスタックエンジニア
- MOpsエンジニア
- フロントエンドマネージャー
- バックエンドエンジニア
- Webフロントエンド
- Webエンジニア
- サーバーサイドエンジニア
- フロントエンジニア
- エンジニア@京都
- エンジニア@大阪
- エンジニア オープンポジション
- エンジニアマネージャー
- Rails@京都
- バックエンド@BFW
- Androidエンジニア
- iOSエンジニア
- クラウドエンジニア
- SRE、インフラエンジニア
- テスト自動化エンジニア
- QAエンジニア
- エンジニアリング
- SRE
- エンジニア職
- コーポレートエンジニア
- マーケティングエンジニア
- Webアナリティクスエンジニア
- SDET
- QA関連職種オープンポジション
- データアナリスト
- セキュリティエンジニア
- コミュニケーションデザイナー
- Ui/ UXデザイナー
- UIデザイナー
- UI/UXデザイナー
- プロダクトデザイナー
- サービスデザイナー
- デザイナーオープンポジション
- グラフィックデザイナー
-
ビジネス
- プロダクトマネージャー
- スクラムマスター
- プロダクトマネージャー
- グローバル採用担当者
- グローバル採用担当
- 金融コンプライアンス
- エンジニア採用担当
- 中途採用担当
- 労務
- 採用オペレーション
- システム監査
- ビジネス採用担当
- 経営企画(予実・IR)
- HRBP
- 法務
- 債権管理/MFK
- セールス・事業開発
- 新規事業開発
- ビジネス職
- フィールドセールス
- セールスマネージャー候補
- インサイドセールス SDR
- インサイドセールス企画
- オンラインセールス
- SaaS営業、MFBC
- インサイドセールス MFBC
- セールス MFBC
- マーケター
- マーケティング
- サービス企画
- データマーケター
- BtoBマーケティングリーダー
- CRMスペシャリスト
- WEBマーケティング(B2B)
- Webマーケティング
- デジタルマーケター
- イベントマーケター
- コンテンツマーケ MFBC
- SEO MFBC
- その他
最近AndroidのViewに思うこと(CustomView編)
マネーフォワードでAndroidエンジニアをしています前田です。
最近、Android開発をしていて思っていることザックリまとめてみました。
結論
CustomView積極的に使おう!
最近思ってること
先日のdroidkaigiにて、yanzmさんが下記の点を発表されていました。
Activity/Fragmentに不要な処理書きたくないどんどんファットになっていくActivityやFragmentになんでも書けばいいやということが多く、どんどんクラスが大きくなっている
そこで、Viewに依存するものはCustomView作ってしまい、できるだけView内で完結させたいと思うようになってきました。
実際、マネーフォワードのアプリでも、Activity/Fragmentがファットになっており、View内で完結するものはCustomViewを作成し処理を移行しています。
CustomViewにすることのメリットは下記の点だと考えています。
各View内で完結する処理はView内に記述することでActivity、Fragmentに処理がほどんどかかれないLayoutファイルの見通しが良くなる繰り返し使うようなViewをCustom化することでコピペ処理が減る
実装してみた
全体的な効率UPのために、下記ライブラリを使用しています。
APIリクエストをCustomViewに
APIリクエストはActivity/Fragmentから行いますが、View単体のみでしか使わないものに関してはView内に閉じ込めています。
画面に依存せずCustomViewを画面に配置してあげるだけでAPIをリクエストして、Viewの生成も行ってくれます。
今回の例では、下記を使って実装します。
(APIはtiqavを使用させて頂きました。)
AsyncTaskLoaderRecyclerView
AsyncTaskLoader
AsyncTaskLoader内ではAPIの呼び出しを行います。
公式のサンプルとほぼ同じです。
APIの呼び出しには、retrofitを使用しています。
public class TiqavApiLoader extends AsyncTaskLoader<List<Tiqav>> {
private List<Tiqav> mTiqavList;
public TiqavApiLoader(Context context) {
super(context);
}
@Override
public List<Tiqav> loadInBackground() {
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint("http://api.tiqav.com")
.setConverter(new GsonConverter(new Gson()))
.build();
TiqavApiService api = restAdapter.create(TiqavApiService.class);
return api.getRandom();
}
@Override
protected void onStartLoading() {
if (mTiqavList != null
&& !mTiqavList.isEmpty()) {
deliverResult(mTiqavList);
return;
}
forceLoad();
}
@Override
public void deliverResult(List<Tiqav> data) {
if (!isReset()) {
if (mTiqavList != null) {
mTiqavList = null;
}
}
mTiqavList = data;
if (isStarted()) {
super.deliverResult(data);
}
}
@Override
protected void onReset() {
super.onReset();
onStopLoading();
if (mTiqavList != null) {
mTiqavList = null;
}
}
}
RecyclerView
CustomViewです。このクラス内では下記処理を行います。
RecyclerViewの初期設定LoaderCallbacksの実装
特徴的なのはLoaderCallbacksをView内で実装していることかと思います。
(LoaderでなくてもAPIを非同期で呼んであげても問題は無いと思います。)
public class TiqavRecyclerView extends RecyclerView implements LoaderManager.LoaderCallbacks<List<Tiqav>> {
public TiqavRecyclerView(Context context) {
this(context, null);
}
public TiqavRecyclerView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public TiqavRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
// View 初期化
private void init(Context context) {
// RecylerViewをGridで表示
final GridLayoutManager glm = new GridLayoutManager(context, 4, VERTICAL, false);
this.setLayoutManager(glm);
}
@Override
public Loader<List<Tiqav>> onCreateLoader(int id, Bundle args) {
return new TiqavApiLoader(getContext());
}
@Override
public void onLoadFinished(Loader<List<Tiqav>> loader, List<Tiqav> data) {
if (data == null) {
// TODO: EmptyView.
return;
}
this.setAdapter(new TiqavRecyclerViewAdapter(data));
}
@Override
public void onLoaderReset(Loader<List<Tiqav>> loader) {
}
private static class TiqavRecyclerViewAdapter extends Adapter<TiqavViewHolder> {
@NonNull
private final List<Tiqav> tiqavList;
private TiqavRecyclerViewAdapter(@NonNull List<Tiqav> tiqavList) {
this.tiqavList = tiqavList;
}
@Override
public TiqavViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
final View view = inflater.inflate(R.layout.view_tiqav_image, parent, false);
return new TiqavViewHolder(view);
}
@Override
public void onBindViewHolder(TiqavViewHolder holder, int position) {
final String imgUrl = tiqavList.get(position).getSourceUrl();
holder.bind(imgUrl);
}
@Override
public int getItemCount() {
return tiqavList.size();
}
}
private static class TiqavViewHolder extends ViewHolder {
private ImageView imgView;
private TextView errorText;
public TiqavViewHolder(View itemView) {
super(itemView);
imgView = (ImageView) itemView.findViewById(R.id.img);
errorText = (TextView) itemView.findViewById(R.id.text);
}
void bind(String imgUrl) {
// 画像取得.
Picasso.with(imgView.getContext())
.load(imgUrl)
.fit()
.into(imgView, new Callback() {
@Override
public void onSuccess() {
errorText.setVisibility(GONE);
}
@Override
public void onError() {
imgView.setVisibility(GONE);
errorText.setVisibility(VISIBLE);
}
});
}
}
}
Activity
ActivityではLoaderManagerにLoaderCallbacksを実装したCustomViewクラスを渡します。
APIの呼び出し自体はLoaderが行い、コールバック処理もView側で行っているため、ActivityはLoaderManagerからLoaderを呼び出すだけです。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TiqavRecyclerView tiqavRecyclerView = new TiqavRecyclerView(this);
setContentView(tiqavRecyclerView);
getSupportLoaderManager().initLoader(tiqavRecyclerView.hashCode(), null, tiqavRecyclerView);
}
実際にやってみると、tiqavのAPIから戻ってくるレスポンスの画像のほとんどがnot foundに。。。( ;∀;)
そのため、「画像ないよ」という文字列だらけに…(´;ω;`)ブワッ
よく使うViewをCustomViewに
Layoutファイルの見通し良くするために使います。
今回は階層を表現するときに区切りとして影を落とすことをよくします。
(5.0以降はelevationで設定できます)
毎回layoutファイルに書いてもいいのですが、ちょっと面倒になってきたのでCustomViewにしてみました。
影はnone, top, bottom, bothに設定できるようにします。
構成はこんな感じです。
shadow shapeファイルres/vaules/attrs.xmlres/layout/view_shadow_wapper.xmlShadowableFrameLayout.javaActivity
shadow shapeファイル
影用のshapeファイルを作成します。
boarder_shadow.xml(上から下に影を落とす用)
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid
android:height="1dp"
android:color="#222222" />
<gradient
android:angle="270"
android:endColor="#00000000"
android:startColor="#33222222" />
</shape>
border_shadow_reverse.xml(下から上に影を落とす用)
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid
android:height="1dp"
android:color="#222222" />
<gradient
android:angle="270"
android:endColor="#33222222"
android:startColor="#00000000" />
</shape>
ret/values/attrs.xml
CustomViewにレイアウトファイルから影を設定できるようにdeclare-styleableを作成します。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ShadowableFrameLayout">
<attr name="shadow">
<flag name="non" value="0" />
<flag name="top" value="1" />
<flag name="bottom" value="2" />
<flag name="both" value="4" />
</attr>
</declare-styleable>
</resources>
res/layout/view_shadow_wapper.xml
ShadowableFrameLayout用レイアウトファイルです。
(レイアウトファイル作らないでShadowableFrameLayout内で影部分のView生成しても実現できます。)
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/shadow_top"
android:layout_width="match_parent"
android:layout_height="4dp"
android:layout_gravity="top|center_horizontal"
android:background="@drawable/border_shadow" />
<View
android:id="@+id/shadow_bottom"
android:layout_width="match_parent"
android:layout_height="4dp"
android:layout_gravity="bottom|center_horizontal"
android:background="@drawable/border_shadow_reverse" />
</merge>
ShadowableFrameLayout.java
CustomViewです。実際に受け取ったflagからどのように影を落とすのか決めています。
public class ShadowableFrameLayout extends FrameLayout {
private static final int FLAG_SHADOW_NON = 0;
private static final int FLAG_SHADOW_TOP = 1;
private static final int FLAG_SHADOW_BOTTOM = 2;
private static final int FLAG_SHADOW_BOTH = 4;
@IntDef({FLAG_SHADOW_NON, FLAG_SHADOW_TOP, FLAG_SHADOW_BOTTOM, FLAG_SHADOW_BOTH})
public @interface ShadowFlag {
}
@InjectView(R.id.shadow_top)
View mShadowTop;
@InjectView(R.id.shadow_bottom)
View mShadowBottom;
public ShadowableFrameLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ShadowableFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ShadowableFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initView(context, attrs, defStyleAttr);
}
/**
* initview.
*
* @param context
* @param attrs
* @param defStyleAttr
*/
private void initView(Context context, AttributeSet attrs, int defStyleAttr) {
final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ShadowableFrameLayout, defStyleAttr, 0);
@ShadowFlag final int flag = array.getInt(R.styleable.ShadowableFrameLayout_shadow, 0);
array.recycle();
final View view = LayoutInflater.from(context).inflate(R.layout.view_shadow_wapper, this, true);
ButterKnife.inject(this, view);
setShadow(flag);
}
/**
* Shadow
*
* @param flag
*/
public void setShadow(@ShadowFlag int flag) {
switch (flag) {
case FLAG_SHADOW_TOP:
mShadowTop.setVisibility(VISIBLE);
mShadowBottom.setVisibility(GONE);
break;
case FLAG_SHADOW_BOTTOM:
mShadowTop.setVisibility(GONE);
mShadowBottom.setVisibility(VISIBLE);
break;
case FLAG_SHADOW_BOTH:
mShadowTop.setVisibility(VISIBLE);
mShadowBottom.setVisibility(VISIBLE);
break;
case FLAG_SHADOW_NON:
default:
mShadowTop.setVisibility(GONE);
mShadowBottom.setVisibility(GONE);
break;
}
}
}
使用例
// ...
<!--上に影をつける-->
<com.moneyforward.mfblogsmple.ui.widget.ShadowableFrameLayout
xmlns:shadowable="http://schemas.android.com/apk/res-auto"
shadowable:shadow="top"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
// ... />
</com.moneyforward.mfblogsmple.ui.widget.ShadowableFrameLayout>
// ...
まとめ
積極的にCustomViewを使うことで、Activity/Fragment内の処理を減らせたり、レイアウトファイルの簡素化を図れるので積極的に使いましょう。
今回使用したサンプルはこちらにおいてあります。
最後に
マネーフォワードでは、積極的に新しい技術や取り組みに挑戦し、ユーザのためになる開発を行いたいエンジニアを募集しています!
みなさまのご応募お待ちしております!
【採用サイト】
■『マネーフォワード採用サイト』 https://recruit.moneyforward.com/
■『Wantedly』 https://www.wantedly.com/companies/moneyforward
【プロダクト一覧】
■家計簿アプリ・クラウド家計簿ソフト『マネーフォワード』 https://moneyforward.com/
■家計簿アプリ・クラウド家計簿ソフト『マネーフォワード』 iPhone,iPad
■家計簿アプリ・クラウド家計簿ソフト『マネーフォワード』 Android
■クラウド型会計ソフト『MFクラウド会計』 https://biz.moneyforward.com/
■クラウド型請求書管理ソフト『MFクラウド請求書』 https://invoice.moneyforward.com/
■クラウド型給与計算ソフト『MFクラウド給与』 https://payroll.moneyforward.com/