一個Json就是一個App

**** 原創(chuàng)不易,轉(zhuǎn)載注明出處 ****

android原生App最大的痛點就是更新周期長,稍有改動,就需要發(fā)布新版本,加上審核,最快也要3天后才能讓用戶看到新模塊。

如果能通過后臺下發(fā)數(shù)據(jù)創(chuàng)建View,執(zhí)行操作,就可以很靈活的動態(tài)控制頁面。

如果下發(fā)json,View的數(shù)據(jù)很好處理,因為android提供的控件類型是有限的,只要枚舉出來就可以。但是一個App的操作太多太多,但是不是無限多呢?也不是。

一、 只有View

假設開發(fā)一個App只需要展示基礎控件。

    {
        "name": "TV",
        "content": "文本",
        "color": "#333333",
        "size": "16"
    }

明顯這是一個TextView,指定了顯示的文字、顏色和大小。

    {
        "name": "IV",
        "width": "100",
        "height": "100",
        "url": "https://bugly.qq.com/v2/image?id=2c06cba9-7d27-4f1c-8b0d-b932c33deaf3"
    }

這個就可以生成一個ImageView。如果更細化,還可以指定margin和padding,腦補css。

同理,如果是容器布局,可以擴展一下,加一個子類集合:

    {
        "name": "VLL",
        "children": [
            {
                "name": "TV"
            },
            {
                "name": "TV"
            },
            {
                "name": "TV"
            }
        ]
    }

"VLL"可以提前協(xié)議好是Vertical的LinearLayout,children就是子View的集合。

有了控件還需要一個頁面承載,頁面也能看成View,但是頁面需要有更多的功能,全放View會導致View的屬性過多,所以也能做一層抽象,

    {
        "contextName": "home",
        "layout": {
        
        }
    }

"contextName"能唯一標記一個頁面,比如登錄頁面可以標記為"login","layout"實質(zhì)就是一個View,字段和上面的基礎控件一致。

有了上面的規(guī)則,現(xiàn)在可以嘗試做一個有三個文本的首頁:

    {
        "contextName": "home",
        "layout": {
            "name": "VLL",
            "children": [
                {
                    "name": "TV",
                    "content": "打開頁面",
                    "color": "#333333",
                    "size": "16"
                },
                {
                    "name": "TV",
                    "content": "彈出Toast",
                    "color": "#333333",
                    "size": "16"
                },
                {
                    "name": "TV",
                    "content": "請求網(wǎng)絡",
                    "color": "#333333",
                    "size": "16"
                }
            ]
        }
    }

二、 View的響應

第一步已經(jīng)能自動填充控件了,但是如果真想點擊第二個TextView去彈出一個Toast,怎么處理呢?可以嘗試在View的數(shù)據(jù)里面指定一個動作:

    {
         "name": "TV",
         "content": "彈出Toast",
         "color": "#333333",
         "size": "16",
         "action": {
            "name": "toast",
            "msg": "彈出一下"
         }
    }

這樣點擊的時候就可以解析出一個Toast的動作。當然Action是需要提前窮舉的,還是前面說的,一個App的動作肯定不是無限的。比如跳轉(zhuǎn)一個頁面:

    {
         "name": "TV",
         "content": "打開頁面",
         "color": "#333333",
         "size": "16",
         "action": {
            "name": "open",
            "nextPage": {
                "contextName": "detail",
                "layout": {}
            }
         }
    }

"nextPage"已經(jīng)能自動生成第二個頁面了。甚至于,請求也是一個Action:

    {
         "name": "TV",
         "content": "請求網(wǎng)絡",
         "color": "#333333",
         "size": "16",
         "action": {
            "name": "request",
            "url": "https://xxx.com",
            "params": {
                "name": "rjp",
                "age": "18"
            }
         }
    }

如果你已經(jīng)封裝了請求,上面的數(shù)據(jù)已經(jīng)夠去請求一下了,但是請求回來的數(shù)據(jù)呢?這就是說,有時候Action的動作是有后續(xù)動作的,有一種嵌套關系:

    {
         "name": "TV",
         "content": "請求網(wǎng)絡",
         "color": "#333333",
         "size": "16",
         "action": {
            "name": "request",
            "url": "https://xxx.com",
            "params": {
                "name": "rjp",
                "age": "18"
            },
            "action": "setData"
         }
    }

"request"的后續(xù)有一個"setData"的動作。這就不好處理了,因為每個頁面的業(yè)務數(shù)據(jù)都是獨特的,數(shù)據(jù)模型無法統(tǒng)一。所以需要一個中間層,能對后臺下發(fā)的數(shù)據(jù)進行標準化輸出:

    public class DataBean {
        private String a;
        private String b;
        private String c;
        private String d;
        private String e;
        private String f;
        private String g;
    }

也就是說,不管我請求哪個接口,返回的數(shù)據(jù)永遠是abcdefg,我也不關心字段究竟代表什么。

那怎么知道下發(fā)的數(shù)據(jù)應該填充到哪個控件呢?可以通過給控件設置一個value,來指定需要的數(shù)據(jù):

    {
         "name": "TV",
         "content": "請求網(wǎng)絡",
         "color": "#333333",
         "size": "16",
         "value": "a",
         "action": {
            "name": "request",
            "url": "https://xxx.com",
            "params": {
                "name": "rjp",
                "age": "18"
            },
            "action": "setData"
         }
    }

這樣點擊完請求數(shù)據(jù),如果數(shù)據(jù)里面帶上了"a": "后臺數(shù)據(jù)",就將數(shù)據(jù)填到這個TextView上。填充首先想到的就是遍歷頁面的根View,但是隨著頁面復雜化,非常耗時,可以參考局部刷新的做法,對有value屬性的View進行緩存,只要遍歷緩存集合就行了,非常高效。

三、 拼多多

說了這么多還沒有一個完整的例子,下面一步步來實現(xiàn)。

    public class PageActivity extends AppCompatActivity implements IPage {
    
        private List<View> viewCache;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_page);
    
            FrameLayout pageContainer = findViewById(R.id.page_container);
            Intent intent = getIntent();
            if (intent != null && intent.hasExtra("nextPage")) {
                String nextPage = intent.getStringExtra("nextPage");
                PageBean pageBean = JSONObject.parseObject(nextPage, PageBean.class);
                if (pageBean != null) {
                    viewCache = new ArrayList<>();
                    pageContainer.addView(LayoutFactory.createView(this, pageBean.getLayout()));
                }
            }
        }
    
        @Override
        public Context getContext() {
            return this;
        }
    
        @Override
        public List<View> getViewCache() {
            return viewCache;
        }
    }

創(chuàng)建一個簡單的頁面容器Activity,布局只有一個FrameLayout,從上一個頁面接收json,這個json描述整個Page,當然也可以通過接口請求獲取,測試階段直接從Assets讀取。

上面的關鍵是獲取到json轉(zhuǎn)成PageBean結(jié)構(gòu):

    public class PageBean {
        private String contextName;
        private ViewBean layout;
    
        public ViewBean getLayout() {
            return layout;
        }
    
        public void setLayout(ViewBean layout) {
            this.layout = layout;
        }
    
        public String getContextName() {
            return contextName;
        }
    
        public void setContextName(String contextName) {
            this.contextName = contextName;
        }
    }

ViewBean:

    public class ViewBean {
    
        private String id;
        private String name;
        private String content;
        private String color;
        private String value;
        private float size = 14.0f;
        private int width;
        private int height;
        private List<ViewBean> children;
        private String action;
        private String url;
        private String itemType;
        
    }

ViewBean存在一個問題就是所有的屬性都糅合在一個數(shù)據(jù)結(jié)構(gòu)里,會造成浪費,解決辦法是一個類型的View給一個Bean,然后設置ViewType,但是那是優(yōu)化時考慮的問題,目前只使用一個。

拿到了Layout就可以通過簡單工廠模式開始渲染布局了:

    public class LayoutFactory {
    
        public static View createView(IPage page, ViewBean viewBean) {
            String name = viewBean.getName();
            switch (name) {
                case ViewType.VLL:
                    return createLinearLayout(page, viewBean, true);
                case ViewType.HLL:
                    return createLinearLayout(page, viewBean, false);
                case ViewType.TV:
                    return createTextView(page, viewBean);
                case ViewType.IV:
                    return createImageView(page, viewBean);
                default:
                    return new View(page.getContext());
            }
        }
    
        private static View createLinearLayout(IPage page, ViewBean viewBean, boolean isVertical) {
            LinearLayout vll = new LinearLayout(page.getContext());
            ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
            vll.setLayoutParams(layoutParams);
            vll.setOrientation(isVertical ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL);
            List<ViewBean> children = viewBean.getChildren();
            if (children != null && children.size() > 0) {
                int size = children.size();
                for (int i = 0; i < size; i++) {
                    vll.addView(createView(page, children.get(i)));
                }
            }
            return vll;
        }
        
        private static View createImageView(IPage page, ViewBean viewBean) {
            ImageView imageView = new ImageView(page.getContext());
            imageView.setTag(R.id.image_tag_id, viewBean);
            ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(viewBean.getWidth(), viewBean.getHeight());
            imageView.setLayoutParams(layoutParams);
            String url = viewBean.getUrl();
            if (!TextUtils.isEmpty(url)) {
                Glide.with(page.getContext()).load(url).into(imageView);
            }
            if (!TextUtils.isEmpty(viewBean.getValue())) {
                List<View> viewCache = page.getViewCache();
                if (viewCache != null) {
                    viewCache.add(imageView);
                }
            }
            return imageView;
        }
    
        /**
         * 創(chuàng)建一個TextView
         *
         * @param page
         * @param viewBean
         * @return
         */
        private static View createTextView(IPage page, ViewBean viewBean) {
            TextView tv = new TextView(page.getContext());
            try {//多個try保證某一個臟數(shù)據(jù)不會導致view整體加載失敗
                tv.setTextColor(Color.parseColor(viewBean.getColor()));
            } catch (Exception e) {
                e.printStackTrace();
                tv.setTextColor(Color.parseColor("#333333"));
            }
            try {
                tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, viewBean.getSize());
            } catch (Exception e) {
                e.printStackTrace();
                tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14.0f);
            }
            if (!TextUtils.isEmpty(viewBean.getValue())) {
                List<View> viewCache = page.getViewCache();
                if (viewCache != null) {
                    viewCache.add(tv);
                }
            }
            tv.setText(viewBean.getContent());
            tv.setTag(viewBean);
            tv.setOnClickListener(v -> {
                ViewBean bean = (ViewBean) v.getTag();
                IAction action = ActionFactory.createAction(page, bean.getAction());
                action.action(bean.getAction());
            });
            return tv;
        }
    }

注意填充的過程中判斷value是否存在,存在直接存到viewCache集合里,后面設置數(shù)據(jù)能用上。

createView的方法傳入了一個IPage接口,這個接口是為了方便獲取context上下文和viewCache集合:

    public interface IPage {
    
        Context getContext();
    
        List<View> getViewCache();
    }

createTextView方法下設置了點擊事件的監(jiān)聽,當點擊的時候會觸發(fā)Action,定義了IAction接口:

    public interface IAction {
        void action(ActionBean action);
    }

在點擊的時候拿到action數(shù)據(jù),通過ActionFactory簡單工廠生成對應的Action實體,先看一個簡單的Toast怎么實現(xiàn):

    public class ToastAction implements IAction {
    
        private IPage page;
    
        public ToastAction(IPage page) {
            this.page = page;
        }
    
        @Override
        public void action(ActionBean action) {
            Toast.makeText(page.getContext(), action.getMsg(), Toast.LENGTH_SHORT).show();
        }
    }

這樣整體App的Toast就都可以通過指定name = "toast"完成了。復雜一點的,比如請求Action之后,設置數(shù)據(jù)Action:

    public class RequestAction implements IAction {
    
        private IPage page;
    
        public RequestAction(IPage page) {
            this.page = page;
        }
    
        @Override
        public void action(ActionBean action) {
            if (action != null) {
                try {
                    Thread.sleep(2_000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                String response = "{\n" +
                        "      \"a\": \"這是通過請求獲取的數(shù)據(jù),真的!\"\n" +
                        "    }";
                String lastAction = action.getAction();
                ActionBean lastActionBean = JSONObject.parseObject(lastAction, ActionBean.class);
                lastActionBean.setResponse(response);
                ActionFactory.createAction(page, lastAction).action(lastActionBean);
            }
        }
    }

因為請求還要接入請求框架,直接模擬請求返回的數(shù)據(jù)了。執(zhí)行設置數(shù)據(jù)Action的時候攜帶上請求回來的response:

    public class SetDataAction implements IAction {
    
        private IPage page;
    
        public SetDataAction(IPage page) {
            this.page = page;
        }
    
        @Override
        public void action(ActionBean action) {
            if (action != null) {
                String response = action.getResponse();
                if (!TextUtils.isEmpty(response)) {
                    DataBean dataBean = JSONObject.parseObject(response, DataBean.class);
                    List<View> viewCache = page.getViewCache();
                    if (viewCache != null && viewCache.size() > 0) {
                        int size = viewCache.size();
                        for (int i = 0; i < size; i++) {
                            View view = viewCache.get(i);
                            bindViewData(view, dataBean);
                        }
                    }
                }
            }
        }
    
        /**
         * 綁定頁面數(shù)據(jù)
         *
         * @param view
         * @param dataBean
         */
        private void bindViewData(View view, DataBean dataBean) {
            if (view instanceof TextView) {
                TextView textView = (TextView) view;
                ViewBean viewBean = (ViewBean) textView.getTag();
                String keyData = dataBean.getData(viewBean.getValue());
                textView.setText(keyData);
            } else if (view instanceof ImageView) {
                //TODO 
            }
        }
    }

不需要反復遍歷根節(jié)點,就能獲取到設置了value = "a"的TextView,再拿到DataBean的a值,綁定上就可以看到結(jié)果了。

復雜的ListView綁定也是可以實現(xiàn)的。

Demo地址已更新

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容