Android Study 之 如何透過Data Binding提升擼
LZ-Says:洗個澡,突然感覺爽到爆~~~ 又回來了哦~
前言
前幾天,終於完善了關於Data Binding基礎篇以及進階篇博文編寫,過程很是艱難哦~
下面附上鍊接地址:
;
;
而今天,整整行囊,準備開啟Data Binding高階篇,完成之後,也該開啟Android重新回味之路了,長線計劃,一定要縮短時間咯~~~
拖了好久咯~~~ MMP呦~
啟程
以下關於Data Binding分析基於Libary 1.3.1。
一、setContentView原始碼分析
首先我們來回顧下Data Binding最初的使用:
DataBindingUtil.setContentView
我們先深入進去,看看在這裡面,它到底幹了什麼?
/** * 設定Activity內容View為給定佈局並返回關聯繫結集。 * 這裡需要注意的是:給定的佈局資源型別必須為非Merge Layout! * * @param activity 當前Activity * @param layoutId 當資源被引入時,繫結並且設定Activity內容 * @return 繫結並關聯引入Content View */ public staticT setContentView(Activity activity, int layoutId) { return setContentView(activity, layoutId, sDefaultComponent); }
可以看到這裡直接return了一個setContentView,並且將我們傳遞的引數繼續傳送。
一起來看看這裡幹了什麼鬼?
public staticT setContentView(Activity activity, int layoutId, DataBindingComponent bindingComponent) { activity.setContentView(layoutId); View decorView = activity.getWindow().getDecorView(); ViewGroup contentView = (ViewGroup) decorView.findViewById(android.R.id.content); return bindToAddedViews(bindingComponent, contentView, 0, layoutId); }
首先,為我們的Activity設定Content View,也就是將要展示的UI;
接著,獲取了一個DecorView,那麼為什麼要獲取它呢?這裡LZ放置一張關於Activity佈局層級關係圖:
可以很清晰的看到,在我們的Activity的層級關係,因此,拿到Content View上級,也就可以理解為拿到了對Content View的控制權。
接下來,直接拿到ContentView的例項,透過:decorView.findViewById(android.R.id.content)。
而最後,則呼叫bindToAddedViews方法進行處理,之後將結果Return。那麼,鑑名其意,我們就可以知道這個方法主要的作用就是:繫結新增我們的View。
而在bindToAddedViews方法中,我們猜測下它會怎麼做?來來來,老鐵想想~
LZ這裡猜測,既然是直接呼叫bindToAddedViews將結果Return,那麼最終這裡面應該是遍歷當前Layout下所有的View,拿到之後進行繫結。
下面一起來看看到底進行了什麼操作吧!
private staticT bindToAddedViews(DataBindingComponent component, ViewGroup parent, int startChildren, int layoutId) { // 獲取子控制元件個數 final int endChildren = parent.getChildCount(); // 獲取要新增繫結的子控制元件 final int childrenAdded = endChildren - startChildren; // 如果只有一個直接進行繫結 if (childrenAdded == 1) { final View childView = parent.getChildAt(endChildren - 1); return bind(component, childView, layoutId); } else { // 多個子控制元件時,迴圈遍歷獲取子控制元件,進行繫結 final View[] children = new View[childrenAdded]; for (int i = 0; i 下面繼續深入bind方法。
/** * Returns the binding for the given layout root or creates a binding if one * does not exist. ** Prefer using the generated Binding's
bind
method to ensure type-safe inflation * when it is known thatroot
has not yet been bound. * * @param root The root View of the inflated binding layout. * @param bindingComponent The DataBindingComponent to use in data binding. * @return A ViewDataBinding for the given root View. If one already exists, the * existing one will be returned. * @throws IllegalArgumentException when root is not from an inflated binding layout. * @see #getBinding(View) */@SuppressWarnings("unchecked") // 執行了未檢查的轉換時的警告,例如當使用集合時沒有用泛型 (Generics) 來指定集合儲存的型別public staticT bind(View root, DataBindingComponent bindingComponent) { T binding = getBinding(root); if (binding != null) { return binding; } Object tagObj = root.getTag(); if (!(tagObj instanceof String)) { throw new IllegalArgumentException("View is not a binding layout"); } else { String tag = (String) tagObj; int layoutId = sMapper.getLayoutId(tag); if (layoutId == 0) { throw new IllegalArgumentException("View is not a binding layout"); } return (T) sMapper.getDataBinder(bindingComponent, root, layoutId); } }@SuppressWarnings("unchecked")static T bind(DataBindingComponent bindingComponent, View[] roots, int layoutId) { return (T) sMapper.getDataBinder(bindingComponent, roots, layoutId); }static T bind(DataBindingComponent bindingComponent, View root, int layoutId) { return (T) sMapper.getDataBinder(bindingComponent, root, layoutId); } 這裡可以看出,bind方法有支援倆種繫結方式,一種是單個View也就是引數中的root,一種是多個View也就是引數中的roots。
bind方法Return型別為T extends ViewDataBinding,也就是說這裡處理後的結果就是我們在例項中實際使用的ActivityXXXBinding。
而sMapper又是什麼?
private static DataBinderMapper sMapper = new DataBinderMapper();下面我們簡單看一下DataBinderMapper裡面內建了什麼內容?從命名上可以得出,這裡存放一些類似Mapper的東西,會不會是相關的配置檔案?或者說是生成的配置檔案呢?一起來看一下~
package android.databinding;import com.hlq.databindingdemo.BR;class DataBinderMapper { // 工程設定最低相容SDK版本 final static int TARGET_MIN_SDK = 22; // 無參構造 public DataBinderMapper() { } // return繫結後的Data Binding public android.databinding.ViewDataBinding getDataBinder(android.databinding.DataBindingComponent bindingComponent, android.view.View view, int layoutId) { switch(layoutId) { case com.hlq.databindingdemo.R.layout.item_love_history_show: return com.hlq.databindingdemo.databinding.ItemLoveHistoryShowBinding.bind(view, bindingComponent); case com.hlq.databindingdemo.R.layout.activity_observable: return com.hlq.databindingdemo.databinding.ActivityObservableBinding.bind(view, bindingComponent); ... } return null; } android.databinding.ViewDataBinding getDataBinder(android.databinding.DataBindingComponent bindingComponent, android.view.View[] views, int layoutId) { switch(layoutId) { } return null; } // 獲取layout Id int getLayoutId(String tag) { // 有效性校驗 if (tag == null) { return 0; } // 獲取Layout ID Tag hashCode值 final int code = tag.hashCode(); switch(code) { case 813835775: { if(tag.equals("layout/item_love_history_show_0")) { return com.hlq.databindingdemo.R.layout.item_love_history_show; } break; } case -1191836097: { if(tag.equals("layout/activity_observable_0")) { return com.hlq.databindingdemo.R.layout.activity_observable; } break; } ... } return 0; } // 將ID幹成String String convertBrIdToString(int id) { if (id = InnerBrLookup.sKeys.length) { return null; } return InnerBrLookup.sKeys[id]; } // 這裡便是在Xml中應用的名稱空間時,我們指定的別名 private static class InnerBrLookup { static String[] sKeys = new String[]{ "_all" ,"bean" ...}; } }接著繼續檢視bind方法中具體執行了什麼操作:
/** * 返回一個繫結後的根佈局或者建立一個不存在的繫結結果 * 當已知root
尚未繫結時,優先使用生成的Binding的bind
方法來確保型別安全的範圍 * @param root 根佈局是引入的繫結的Layout * @param bindingComponent 用於資料繫結的DataBindingComponent * @return 指定根檢視的ViewDataBinding。如果已經存在,現有的將被返回 * @throws 當引入Layout不輸入繫結型別則丟擲IllegalArgumentException * @see #getBinding(View) */@SuppressWarnings("unchecked")public staticT bind(View root, DataBindingComponent bindingComponent) { // 獲取繫結 T binding = getBinding(root); // 如果不等於空,直接返回現有 if (binding != null) { return binding; } // 獲取引入Layout tag Object tagObj = root.getTag(); if (!(tagObj instanceof String)) { throw new IllegalArgumentException("View is not a binding layout"); } else { String tag = (String) tagObj; int layoutId = sMapper.getLayoutId(tag); if (layoutId == 0) { throw new IllegalArgumentException("View is not a binding layout"); } return (T) sMapper.getDataBinder(bindingComponent, root, layoutId); } } 看一下getBinding這裡又是什麼鬼?
/** * 檢索負責給定檢視佈局根的繫結。如果沒有繫結,則返回null
,否則將DataBindingComponent設定成預設:{@link #setDefaultComponent(DataBindingComponent)} * * @param view 具有繫結的佈局中的根View
* @return 如果不是bind檢視或者沒有進行關聯繫結直接返回null */public staticT getBinding(View view) { return (T) ViewDataBinding.getBinding(view); }static ViewDataBinding getBinding(View v) { if (v != null) { // 校驗當前最低相容版本是否大於等於api 14 DataBinderMapper.TARGET_MIN_SDK >= 14 if (USE_TAG_ID) { // 直接return獲取到的tag return (ViewDataBinding) v.getTag(R.id.dataBinding); } else { // return是ViewDataBinding型別的tag final Object tag = v.getTag(); if (tag instanceof ViewDataBinding) { return (ViewDataBinding) tag; } } } return null; }/** * 返回與此檢視關聯的標記和指定的鍵。 * * @param key tag對應key * * @return Object儲存在此檢視中作為標記,如果未設定,則返回null * * @see #setTag(int, Object) * @see #getTag() */public Object getTag(int key) { if (mKeyedTags != null) return mKeyedTags.get(key); return null; } 簡單瞭解下關於getLayoutId方法:
int getLayoutId(String tag) {if (tag == null) { return 0; }final int code = tag.hashCode();switch(code) { case 813835775: { if(tag.equals("layout/item_love_history_show_0")) { return com.hlq.databindingdemo.R.layout.item_love_history_show; } break; } case -1191836097: { if(tag.equals("layout/activity_observable_0")) { return com.hlq.databindingdemo.R.layout.activity_observable; } break; } ... }return 0; }根據Tag的hashCode返回對應layout。
到此,setContentView分析結束一個段落。
針對之前關於Data Binding的使用,結合我們剛剛分析的結果,我們簡單回顧下:
首先,改造佈局,也就是新增layout,此處需要注意,不能是merge佈局;
接著拿到當前Activity、給定的Layout以及預設的DataBindingComponent。獲取到Content View父級,也就是DecorView,目前取到控制權;
而最後,則是透過遍歷ChildView新增並繫結即可。當然這裡面忽略了很多細節,例如我們的tag取值,具體繫結操作。有興趣可自行了解下,LZ這裡就不過多的闡述了。
二、inflate分析
上述講述了setContentView,這裡一起來看下關於infate原始碼,看看他們之間又何異同性?
/** * 引用一個繫結佈局並返回該佈局的新建立的繫結。 * 這使用了設定的DataBindingComponent * {@link #setDefaultComponent(DataBindingComponent)}. * * 除非layoutId
是未知的,才使用此版本。否則,使用生成的繫結的引用方法來確保型別安全的引用 * * @param inflater The LayoutInflater used to inflate the binding layout. * @param layoutId The layout resource ID of the layout to inflate. * @param parent Optional view to be the parent of the generated hierarchy * (if attachToParent is true), or else simply an object that provides * a set of LayoutParams values for root of the returned hierarchy * (if attachToParent is false.) * @param attachToParent Whether the inflated hierarchy should be attached to the * parent parameter. If false, parent is only used to create * the correct subclass of LayoutParams for the root view in the XML. * @return The newly-created binding for the inflated layout ornull
if * the layoutId wasn't for a binding layout. * @throws InflateException When a merge layout was used and attachToParent was false. * @see #setDefaultComponent(DataBindingComponent) */public staticT inflate(LayoutInflater inflater, int layoutId, @Nullable ViewGroup parent, boolean attachToParent) { return inflate(inflater, layoutId, parent, attachToParent, sDefaultComponent); }/** * Inflates a binding layout and returns the newly-created binding for that layout. * * Use this version only if
layoutId
is unknown in advance. Otherwise, use * the generated Binding's inflate method to ensure type-safe inflation. * * @param inflater The LayoutInflater used to inflate the binding layout. * @param layoutId The layout resource ID of the layout to inflate. * @param parent Optional view to be the parent of the generated hierarchy * (if attachToParent is true), or else simply an object that provides * a set of LayoutParams values for root of the returned hierarchy * (if attachToParent is false.) * @param attachToParent Whether the inflated hierarchy should be attached to the * parent parameter. If false, parent is only used to create * the correct subclass of LayoutParams for the root view in the XML. * @param bindingComponent The DataBindingComponent to use in the binding. * @return The newly-created binding for the inflated layout ornull
if * the layoutId wasn't for a binding layout. * @throws InflateException When a merge layout was used and attachToParent was false. */public staticT inflate( LayoutInflater inflater, int layoutId, @Nullable ViewGroup parent, boolean attachToParent, DataBindingComponent bindingComponent) { final boolean useChildren = parent != null && attachToParent; final int startChildren = useChildren ? parent.getChildCount() : 0; final View view = inflater.inflate(layoutId, parent, attachToParent); if (useChildren) { return bindToAddedViews(bindingComponent, parent, startChildren, layoutId); } else { return bind(bindingComponent, view, layoutId); } } 而這裡的邏輯,則相對簡單一些判斷是否ContentView有內容,有則需要逐個獲取並新增繫結,無則直接繫結即可。
而下面的操作流程則與setContentView一樣了。
一個相當於直接引用佈局,直接拿到了佈局的控制權,而另一個則是需要去獲取上級,透過獲取上級拿到控制權進行操作。
三、生成Util類原始碼閱讀
怎麼看原始碼呢?
首先看一波LZ例項中的MainActivity,經過Data Binding轉化後如下:
//// Source code recreated from a .class file by IntelliJ IDEA// (powered by Fernflower decompiler)//package com.hlq.databindingdemo.databinding;import android.databinding.DataBindingComponent;import android.databinding.DataBindingUtil;import android.databinding.ViewDataBinding;import android.databinding.ViewDataBinding.IncludedLayouts;import android.support.annotation.NonNull;import android.support.annotation.Nullable;import android.util.SparseIntArray;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.view.View.OnClickListener;import android.widget.Button;import android.widget.ScrollView;import com.hlq.databindingdemo.MainActivity.Presenter;// 繼承ViewDataBindingpublic class ActivityMainBinding extends ViewDataBinding { @Nullable private static final IncludedLayouts sIncludes = null; @Nullable private static final SparseIntArray sViewsWithIds = null; @NonNull public final Button bindData; @NonNull public final Button bingListener; @NonNull public final Button imageView; ... @NonNull private final ScrollView mboundView0; @Nullable private Presenter mPersenter; private ActivityMainBinding.OnClickListenerImpl mPersenterOnClickAndroidViewViewOnClickListener; private long mDirtyFlags = -1L; public ActivityMainBinding(@NonNull DataBindingComponent bindingComponent, @NonNull View root) { super(bindingComponent, root, 0); // 從map中讀取配置資訊 這裡麵包含View中的控制元件等 Object[] bindings = mapBindings(bindingComponent, root, 13, sIncludes, sViewsWithIds); // 例項化以及設定tag this.bindData = (Button)bindings[1]; this.bindData.setTag((Object)null); ... this.setRootTag(root); // 進行view的非同步重新整理 this.invalidateAll(); } // 執行View非同步重新整理 public void invalidateAll() { synchronized(this) { this.mDirtyFlags = 2L; } this.requestRebind(); } // 判斷是否有Observable化的欄位資料被更新 public boolean hasPendingBindings() { synchronized(this) { return this.mDirtyFlags != 0L; } } // 設定xml中引用的viewModel public boolean setVariable(int variableId, @Nullable Object variable) { boolean variableSet = true; if (12 == variableId) { this.setPersenter((Presenter)variable); } else { variableSet = false; } return variableSet; } // 設定事件 public void setPersenter(@Nullable Presenter Persenter) { this.mPersenter = Persenter; // 設定同步鎖 synchronized(this) { this.mDirtyFlags |= 1L; } this.notifyPropertyChanged(12); super.requestRebind(); } @Nullable public Presenter getPersenter() { return this.mPersenter; } protected boolean onFieldChange(int localFieldId, Object object, int fieldId) { return false; } // 執行繫結 protected void executeBindings() { long dirtyFlags = 0L; synchronized(this) { dirtyFlags = this.mDirtyFlags; this.mDirtyFlags = 0L; } Presenter persenter = this.mPersenter; OnClickListener persenterOnClickAndroidViewViewOnClickListener = null; if ((dirtyFlags & 3L) != 0L && persenter != null) { persenterOnClickAndroidViewViewOnClickListener = (this.mPersenterOnClickAndroidViewViewOnClickListener == null ? (this.mPersenterOnClickAndroidViewViewOnClickListener = new ActivityMainBinding.OnClickListenerImpl()) : this.mPersenterOnClickAndroidViewViewOnClickListener).setValue(persenter); } if ((dirtyFlags & 3L) != 0L) { this.bindData.setOnClickListener(persenterOnClickAndroidViewViewOnClickListener); this.bingListener.setOnClickListener(persenterOnClickAndroidViewViewOnClickListener); this.imageView.setOnClickListener(persenterOnClickAndroidViewViewOnClickListener); this.normalRecyclerView.setOnClickListener(persenterOnClickAndroidViewViewOnClickListener); this.observableFieldStudy.setOnClickListener(persenterOnClickAndroidViewViewOnClickListener); this.showLoveHistory.setOnClickListener(persenterOnClickAndroidViewViewOnClickListener); this.showLoveHistoryOnClick.setOnClickListener(persenterOnClickAndroidViewViewOnClickListener); this.theWordForMe.setOnClickListener(persenterOnClickAndroidViewViewOnClickListener); this.updateData.setOnClickListener(persenterOnClickAndroidViewViewOnClickListener); this.useExpression.setOnClickListener(persenterOnClickAndroidViewViewOnClickListener); this.useInclude.setOnClickListener(persenterOnClickAndroidViewViewOnClickListener); this.useViewStub.setOnClickListener(persenterOnClickAndroidViewViewOnClickListener); } } @NonNull public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater, @Nullable ViewGroup root, boolean attachToRoot) { return inflate(inflater, root, attachToRoot, DataBindingUtil.getDefaultComponent()); } @NonNull public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater, @Nullable ViewGroup root, boolean attachToRoot, @Nullable DataBindingComponent bindingComponent) { return (ActivityMainBinding)DataBindingUtil.inflate(inflater, 2131296288, root, attachToRoot, bindingComponent); } @NonNull public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater) { return inflate(inflater, DataBindingUtil.getDefaultComponent()); } @NonNull public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater, @Nullable DataBindingComponent bindingComponent) { return bind(inflater.inflate(2131296288, (ViewGroup)null, false), bindingComponent); } // 繫結檢視 @NonNull public static ActivityMainBinding bind(@NonNull View view) { return bind(view, DataBindingUtil.getDefaultComponent()); } // 驗證當前佈局是否為Binding指定,如果是則直接Return繫結後實體,反之丟擲異常 @NonNull public static ActivityMainBinding bind(@NonNull View view, @Nullable DataBindingComponent bindingComponent) { if (!"layout/activity_main_0".equals(view.getTag())) { throw new RuntimeException("view tag isn't correct on view:" + view.getTag()); } else { return new ActivityMainBinding(bindingComponent, view); } } // 直接實現系統點選事件 public static class OnClickListenerImpl implements OnClickListener { private Presenter value; public OnClickListenerImpl() { } // 搞了一個實現,用於設定Value,也就是value的初始化 public ActivityMainBinding.OnClickListenerImpl setValue(Presenter value) { this.value = value; return value == null ? null : this; } // 點選事件初始化 public void onClick(View arg0) { this.value.onClick(arg0); } } }下面針對其中幾個方法進行原始碼閱讀分析:
mapBindings:遍歷檢視層次結構
Object[] bindings = mapBindings(bindingComponent, root, 13, sIncludes, sViewsWithIds);這裡為啥是13?是因為LZ介面中放置了1個TextView,12個Button按鈕。
/** * 在根下遍歷檢視層次結構,並將標記的檢視,包含檢視和帶有ID的檢視拖放到返回的Object[]中。這用於遍歷檢視層以查詢所有繫結和ID檢視。 * * @param bindingComponent 用於此繫結的繫結元件 * @param roots 檢視層次結構的根檢視層級。 這與合併標籤一起使用 * @param numBindings ID'd檢視的總數,包含表示式的檢視和包含 * @param includes 包含佈局資訊,由它們的容器索引索引 * @param viewsWithIds 沒有標籤但擁有ID的檢視索引 * @return 大小為numBindings的陣列包含層次結構中具有ID(在viewsWithIds中具有元素)的所有檢視,都被標記為包含表示式或包含的佈局的繫結。 * @hide */protected static Object[] mapBindings(DataBindingComponent bindingComponent, View[] roots, int numBindings, IncludedLayouts includes, SparseIntArray viewsWithIds) { Object[] bindings = new Object[numBindings]; for (int i = 0; i
setRootTag:設定Root標籤
setTag版本相容:
protected void setRootTag(View view) { if (USE_TAG_ID) { // DataBinderMapper.TARGET_MIN_SDK >= 14 view.setTag(R.id.dataBinding, this); } else { view.setTag(this); } }SparseArray,看這個,果然,怪不得之前聽說推薦使用SparseArray。有時間得看看咯。
public void setTag(int key, final Object tag) { // If the package id is 0x00 or 0x01, it's either an undefined package // or a framework id if ((key >>> 24) (2); } mKeyedTags.put(key, tag); }
invalidateAll:執行View非同步重新整理
透過呼叫requestRebind,執行View非同步重新整理。
public void invalidateAll() { synchronized(this) { this.mDirtyFlags = 2L; } this.requestRebind(); }
requestRebind:強制View非同步重新整理
protected void requestRebind() { // 如果bind有值,直接執行非同步View更新 if (mContainingBinding != null) { mContainingBinding.requestRebind(); } else { // 開啟同步鎖 synchronized (this) { if (mPendingRebind) { return; } mPendingRebind = true; } // 版本相容 SDK_INT >= 16 if (USE_CHOREOGRAPHER) { // 釋出幀回撥以在下一幀上執行 也就是View更新 mChoreographer.postFrameCallback(mFrameCallback); } else { // 將Runnable r被新增到訊息佇列中。Runnable將在該處理程式所連線的執行緒上執行。 mUIThreadHandler.post(mRebindRunnable); } } }
postFrameCallback:傳送Frame回撥
// Posts a callback to run on the next framepublic void postFrameCallback(FrameCallback callback) { postFrameCallbackDelayed(callback, 0); }// 回撥型別:提交回撥。處理幀的繪製後操作。 // 遍歷完成後執行。 報告的{@link #getFrameTime()幀時間} // 在此回撥期間可能會更新以反映在遍歷正在進行時發生的延遲,以防重型佈局操作導致某些幀被跳過。// 在此回撥期間報告的幀時間提供更好的估計動畫(以及檢視層次狀態的其他更新)實際生效的幀的開始時間。public static final int CALLBACK_COMMIT = 3;private static final int CALLBACK_LAST = CALLBACK_COMMIT;// 釋出回撥以在指定延遲後的下一幀上執行。回撥執行一次後自動刪除。public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) { if (callback == null) { throw new IllegalArgumentException("callback must not be null"); } postCallbackDelayedInternal(CALLBACK_ANIMATION, callback, FRAME_CALLBACK_TOKEN, delayMillis); }private void postCallbackDelayedInternal(int callbackType, Object action, Object token, long delayMillis) { if (DEBUG_FRAMES) { Log.d(TAG, "PostCallback: type=" + callbackType + ", action=" + action + ", token=" + token + ", delayMillis=" + delayMillis); } // 開啟同步鎖 synchronized (mLock) { // 獲取的是系統的時間 final long now = SystemClock.uptimeMillis(); final long dueTime = now + delayMillis; // Choreographer機制,用於同Vsync機制配合,實現統一排程介面繪圖. // 新增佇列鎖 mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token); if (dueTime不行了,暈死了,太多太多不懂了。。。
MMP呦,得趕緊開啟Android重走路了。。。
結束語
原本打算挨個原始碼解析下,沒想到看的看的發現,我日,太多東西了,想一篇文章搞定?對於目前的LZ而言,太過於困難。與其繼續死摳,不如趕緊加強基礎,練好基本功,再來攻克Boss。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/964/viewspace-2806564/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 深入原始碼學習 Android data binding 之:回撥通知管理器 CallbackRegistry 解析原始碼Android
- Android開發教程 - 使用Data Binding(五)資料繫結Android
- Android開發教程 - 使用Data Binding(八)使用自定義InterfaceAndroid
- Android開發教程 - 使用Data Binding(三)在Activity中的使用Android
- Android開發教程 - 使用Data Binding(四)在Fragment中的使用AndroidFragment
- Jetpack ---- Data Binding入門(二)Jetpack
- WPF Custom control and display binding and specific data
- 【Python】透過Cython提升效能Python
- 如何透過華為分析提升產品留存率?
- 如何透過Spring Data/EntityManager/Session直接獲取DTO資料?SpringSession
- Android開發教程 - 使用Data Binding(七)使用BindingAdapter簡化圖片載入AndroidAPT
- 如何透過Android手機查詢IP地址Android
- 如何透過郵件標題提升EDM轉化率
- 如何透過個性化功能提升專案管理效率?專案管理
- 如何透過線上CRM提升企業競爭力?
- Android開發教程 - 使用Data Binding Android Studio不能正常生成相關類/方法的解決辦法Android
- 看板管理解析:如何透過看板提升專案管理效率?專案管理
- 如何透過A/B測試提升Push推送訊息點選率?
- [譯] Data Binding 庫使用的經驗教訓
- 測試自己 - 透過重構提升自己
- Lyft如何透過DevOps提升擴充套件微服務的生產力? - Garrettdev套件微服務
- 如何透過思維導圖讓你的專案管理高效提升50%?專案管理
- TUV 南德解析如何透過高質量資訊披露提升ESG評級
- 擼擼Android的羊毛(二)----Activity生命週期Android
- 擼擼Android的羊毛(一)----Activity啟動模式Android模式
- Binding(二):控制元件關聯和程式碼提升控制元件
- 怎麼透過CRM來提升客戶轉化率?
- 透過打包 Flash Attention 來提升 Hugging Face 訓練效率Hugging Face
- 一文詳解如何在 ChengYing 中透過產品線部署一鍵提升效率
- 如何透過DBeaver 連線 TDengine?
- 如何透過kubernetes-部署RabbitMQMQ
- 透過CRM分析使用者喜好 提升營銷體驗
- Android 提升使用者體驗之骨架屏Android
- Android 透過scheme跳轉支付寶實現支付AndroidScheme
- Android透過接收UDP訊息改寫程式配置AndroidUDP
- Android 13及以上如何備份Android/data目錄中的檔案Android
- 如何透過“推送文案的千人千面”有效提升使用者轉化和留存
- Study