一个更好的交互教程

https://github.com/krisc/events/wiki/Android-App-Development-with-Clojure:-An-Interactive-Tutorial

Android App Development with Clojure: An Interactive Tutorial

I've been programming in Java since I was an undergrad in college in 2006. While working a contract job in 2011-2012 in which I was hired to work with a huge mess of Java code1, I was left wondering if Java was rotting my brain. Surely, there has got to be a better way. After an impractical detour2, I decided to take on learningClojure, a Lisp dialect for the JVM. After a two-year journey of hacking personal projects, Clojure is now my general programming language of choice.

While learning how to write apps forAndroid, I was back to programming in Java and again was left thinking that there has got to be a better way! I looked into developing Android apps using Clojure. Although there is still much room for maturation, the efforts ofDaniel Solano Gómez,Alex Yakuchev, andZach Oakesshow a promising future for Clojure in Android development.

Prerequisites

This tutorial is directed towards Clojure programmers who are seeking an alternative to the Java language for Android development. I will assume that you already know the basics of Clojure3and Android.

We will be using Alex Yakushev'slein-droidtool for project management. We will also be using Alex's fork of Daniel Solano Gómez'sneko librarywhich provides function wrappers and alternatives to the Android Java API. However, neko does not replace everything as of the time of this writing and is subject to change so keep theAndroid docshandy. There will be some Java interop in this tutorial. We will be usingemacswith thenreplplugin for this tutorial.

Be forewarned:some tools in this setup are still very young and are in fast development. New versions may pop up as of the time of this writing (Sep 25, 2013) and may introduce breaking changes. For your information, here are the versions of the tools that I am using:

Linux Mint 14 (Nadia) Java 1.6.0_45 clojure-android/clojure 1.5.1-SNAPSHOT Leiningen 2.3.1 lein-droid 0.2.0-beta3 Android SDK Tools 22.0.5 nrepl 0.2.0-bigstack neko 3.0.0-preview1 clojure-complete 0.3.0-SNAPSHOT

Now that you have beenforewarned, let's begin.

What are we making?

Let's make a simple event listing app. This will not be a full-fledged calendar app, but rather a simple tool to pencil in events and have them sorted in chronological order. I have a text file on my desktop that I use to pencil in dates for gigs, practices, and other events.

Android App Development with Clojure_第1张图片

Our app will be based off of this simple idea.

Using lein-droid to setup our project

Alex'sTutorialis a good introduction tolein-droid. Skim through the tutorial to familiarize yourself with the basiclein-droidcommands.

This is how my~/.lein/profiles.cljlooks like:

{:user {:plugins [[lein-droid "0.2.0-beta3"]] :android {:sdk-path "/home/kris/adt-bundle-linux-x86_64-20130522/sdk/"}}}

NOTE: Change the directory to reflect your own sdk's path. And if there is a later version oflein-droid, consider using that.

Run this command at the terminal:

lein droid new events org.stuff.events :activity MyActivity :target-sdk 15 :app-name EventsListing

This will create a template file structure for an Android app. Open theproject.cljfile and change thenekoversion in:dependenciesto"3.0.0-preview1".

If you have an actual Android device at hand, connect it to your computer. If not, you can setup anemulator. Now runlein droid doallat the terminal. This will build the app, install the app to you device, and open annREPLserver within our running app.

Define the Layout

Let's open the main Clojure source file located at./src/events/src/clojure.main.cljinemacsand start defining the layout and the application. Now run this inemacs:M-x nrepland enter the local machine for 'Host' and '9999' for 'Port'. Now you have a REPL inemacswhich is connected to your running app. As you will see in a bit, this is neat-o torpedo.

To start evaluating definitions within our app's namespace, enter this command into the REPL:(in-ns 'org.stuff.events.main)Then, evaluate thensform in the source file by moving the cursor after the closing parenthesis of thensform and hittingC-x C-e.

Let's now code a definition for the layout of the app. Themake-uimacro takes in a vector of elements which will be transformed into XML (learn more here). This structure can be anonymously passed intomake-ui, but let's give it a named definition:

(def main-layout [:linear-layout {:orientation :vertical} [:edit-text {:hint "Event name"}] [:edit-text {:hint "Event location"}]])

Evaluate thisdefform. Let's change thedefactivityform to look like this:

(defactivity org.stuff.events.MyActivity :def a :on-create (fn [this bundle] (on-ui (set-content-view! a (make-ui main-layout)))))

Interactive Development

To demonstrate the power of REPL driven development, move your cursor after the closing parenthesis of theon-uiform (after the third-to-last)from the end), then hitC-x C-e.

[pic here]

After you have finished geeking out on how cool what just happened was, let's add a button to the layout. Ourdefform formain-layoutshould now look like this:

(def main-layout [:linear-layout {:orientation :vertical} [:edit-text {:hint "Event name"}] [:edit-text {:hint "Event location"}] [:button {:text "+ Event"}]])

Evaluate this form. Then evaluate theon-uiform to update the app. (From now on, you can assume that newly added code should be evaluated.)

[pic]

Adding Functionality

The app doesn't really do anything right now. Let's add attributes to our layout elements for functionality.

(declare android.widget.LinearLayout mylayout) (def main-layout [:linear-layout {:orientation :vertical, :id-holder :true :def `mylayout} [:edit-text {:hint "Event name", :id ::name}] [:edit-text {:hint "Event location", :id ::location}] [:button {:text "+ Event"}]])

In order to access the layout by name, we added a:defattribute to ourmain-layout, an:id-holderflag, and a forward declaration form near the top of the source file. Additionally, ouredit-textelements have:idattributes with a keyword value. Thedeclareform allows us later to compile this code using AOT (more on usingleinto build later).

With these additions, we can now access the values of these elements using.getTag. Enter some text into the edit-text fields in the running app then try these at the REPL:

[pic here]

=> (.getTag mylayout) => (str (.getText (::name (.getTag mylayout)))) => (str (.getText (::location (.getTag mylayout))))

Let's write a helper function for our convenience:

(defn get-elmt [elmt] (str (.getText (elmt (.getTag mylayout)))))

Now let's have that button do some work.

(declare android.widget.LinearLayout mylayout) (declare add-event) (def main-layout [:linear-layout {:orientation :vertical, :id-holder :true, :def `mylayout} [:edit-text {:hint "Event name", :id ::name}] [:edit-text {:hint "Event location", :id ::location}] [:button {:text "+ Event", :on-click (fn [_] (add-event))}]])

We added an:on-clickattribute to our:buttonelement whose value is a callback function. Note the forward declaration for that callback function.

Well, we know that we need to add an event to the listing. First, let's add a new element to the layout that will contain the listing. We will use an atom4to hold the current state of the listing.

(defn mt-listing [] (atom "")) (def listing (mt-listing)) (def main-layout [:linear-layout {:orientation :vertical, :id-holder :true, :def `mylayout} [:edit-text {:hint "Event name", :id ::name}] [:edit-text {:hint "Event location", :id ::location}] [:button {:text "+ Event", :on-click (fn [_] (add-event))}] [:text-vew {:text @listing, :id ::listing}]])

Here, we have a constructor which returns a new atom (an empty string). This enables us to have multiple listing objects to work with if we want to. In this tutorial, we will have one listing defined by(def listing (mt-listing)). The value of the listing atom is used for the :text attribute of the newly added :text-view element.

Before we define the callback function, let's play with the REPL and figure out what we actually want to do when the user hits that button. First, we want to update the listing atom with the contents of the:edit-textfields. Enter an event in the running app then run this in the REPL:

[pic here]

=> (swap! listing str (get-elmt ::location) " - " (get-elmt ::name) "\n")

Next, we want to update the ui with the listing.

=> (on-ui (.setText (::listing (.getTag mylayout)) @listing))

Let's write another helper function for setting the text of our elements.

(defn set-elmt [elmt s] (on-ui (.setText (elmt (.getTag mylayout)) s)))

Let's have our callback function perform these two tasks:

(defn add-event [] (swap! listing str (get-elmt ::location) " - " (get-elmt ::name) "\n") (set-elmt ::listing @listing))

Now try hitting that button. Cool, huh?

[pic here]

If you need to clear your listing, just run(def listing (mt-listing)).

Hitting the button should also clear the edit fields. Let's write a function to take care of all that.

(defn update-ui [] (set-elmt ::listing @listing) (set-elmt ::location "") (set-elmt ::name ""))

And let's have ouradd-eventfunction call this.

(defn add-event [] (swap! listing str (get-elmt ::location) " - " (get-elmt ::name) "\n") (update-ui))

If you're coding along at home (and I hope you are!), here is what our code should look like so far:

(ns org.stuff.events.main (:use [neko.activity :only [defactivity set-content-view!]] [neko.threading :only [on-ui]] [neko.ui :only [make-ui]] [neko.application :only [defapplication]])) (declare android.widget.LinearLayout mylayout) (declare add-event) (defn mt-listing [] (atom "")) (def listing (mt-listing)) (def main-layout [:linear-layout {:orientation :vertical, :id-holder :true, :def `mylayout} [:edit-text {:hint "Event name", :id ::name}] [:edit-text {:hint "Event location", :id ::location}] [:button {:text "+ Event", :on-click (fn [_](add-event))}] [:text-view {:text @listing, :id ::listing}]]) (defn get-elmt [elmt] (str (.getText (elmt (.getTag mylayout))))) (defn set-elmt [elmt s] (on-ui (.setText (elmt (.getTag mylayout)) s))) (defn update-ui [] (set-elmt ::listing @listing) (set-elmt ::location "") (set-elmt ::name "")) (defn add-event [] (swap! listing str (get-elmt ::location) " - " (get-elmt ::name) "\n") (update-ui)) (defactivity org.stuff.events.MyActivity :def a :on-create (fn [this bundle] (on-ui (set-content-view! a (make-ui main-layout)))))

So far so good... So what? It might be a good idea to build your app right now by running at the terminallein droid doall. Note that you may have to connect your REPL again after you run this command.

Just One Little Fix

If you rotated your screen, you may have noticed that the listing disappears. Let's fix that, shall we?

(defactivity org.stuff.events.MyActivity :def a :on-create (fn [this bundle] (on-ui (set-content-view! a (make-ui main-layout))) (on-ui (set-elmt ::listing @listing))))

Rotate your screen. The REPL never ceases to amaze me.

The Date Picker

What's the point of an event listing without sorted dates? Let's use java interop to make a date picker5.

[pic of datepicker]

First, let's add some imports into ournsform:

(ns org.stuff.events.main (:use [neko.activity :only [defactivity set-content-view!]] [neko.threading :only [on-ui]] [neko.ui :only [make-ui]] [neko.application :only [defapplication]]) (:import (java.util Calendar) (android.app Activity) (android.app DatePickerDialog DatePickerDialog$OnDateSetListener) (android.app DialogFragment)))

Before we continue, let's change thedefactivityform a bit so we can access our activity outside of this form.

(defactivity org.stuff.events.MyActivity :on-create (fn [this bundle] (on-ui (set-content-view! myActivity (make-ui main-layout))) (on-ui (set-elmt ::listing @listing))))

Note that when the:defattribute is removed, we can refer to ourMyActivitybymyActivity. This is how we will refer to our activity in the upcoming function. Also note that re-evaluating this form may not be enough since the:on-createcallback needs to be called. Rotating your screen will do!

Now we will useproxyto create an instance of an anonymous class:

(defn date-picker [] (proxy [DialogFragment DatePickerDialog$OnDateSetListener] [] (onCreateDialog [savedInstanceState] (let [c (Calendar/getInstance) year (.get c Calendar/YEAR) month (.get c Calendar/MONTH) day (.get c Calendar/DAY_OF_MONTH)] (DatePickerDialog. myActivity this year month day))) (onDateSet [view year month day])))

Calling this function creates an instance of adate-pickerobject. Let's add a new button to the layout that will create then show this dialog.

(declare date-picker) (declare show-picker) (def main-layout [:linear-layout {:orientation :vertical, :id-holder :true, :def `mylayout} [:edit-text {:hint "Event name", :id ::name}] [:edit-text {:hint "Event location", :id ::location}] [:button {:text "...", :on-click (fn [_] (show-picker (date-picker)))}] [:button {:text "+ Event", :on-click (fn [_](add-event))}] [:text-view {:text @listing, :id ::listing}]]) (defn show-picker [dp] (. dp show (. myActivity getFragmentManager) "datePicker"))

Update youruiand try hitting that...button. Now let's have that dialog update a:text-viewwith that chosen date.

(def main-layout [:linear-layout {:orientation :vertical, :id-holder :true, :def `mylayout} [:edit-text {:hint "Event name", :id ::name}] [:edit-text {:hint "Event location", :id ::location}] [:linear-layout {:orientation :horizontal} [:text-view {:hint "Event date", :id ::date}] [:button {:text "...", :on-click (fn [_] (show-picker (date-picker)))}]] [:button {:text "+ Event", :on-click (fn [_](add-event))}] [:text-view {:text @listing, :id ::listing}]])

Note that the new:text-viewelement and the button that spawns the picker are inside a nested:linear-layoutelement. Our date string will have the YYYYMMDD format. Now let's fill out that listener function in our proxy object.

(defn date-picker [] (proxy [DialogFragment DatePickerDialog$OnDateSetListener] [] (onCreateDialog [savedInstanceState] (let [c (Calendar/getInstance) year (.get c Calendar/YEAR) month (.get c Calendar/MONTH) day (.get c Calendar/DAY_OF_MONTH)] (DatePickerDialog. myActivity this year month day))) (onDateSet [view year month day] (on-ui (.setText (::date (.getTag mylayout)) (str year (format "%02d" (inc month)) (format "%02d" day)))))))

Now try thedate-pickeragain. Let's changeadd-eventto include the date.

(defn add-event [] (swap! listing str (get-elmt ::date) " - " (get-elmt ::location) " - " (get-elmt ::name) "\n") (update-ui))

Here's what our source file looks like so far:

(ns org.stuff.events.main (:use [neko.activity :only [defactivity set-content-view!]] [neko.threading :only [on-ui]] [neko.ui :only [make-ui]] [neko.application :only [defapplication]]) (:import (java.util Calendar) (android.view View) (android.app Activity) (android.app DatePickerDialog DatePickerDialog$OnDateSetListener) (android.app DialogFragment))) (declare android.widget.LinearLayout mylayout) (declare add-event) (declare date-picker) (declare show-picker) (defn mt-listing [] (atom "")) (def listing (mt-listing)) (def main-layout [:linear-layout {:orientation :vertical, :id-holder :true, :def `mylayout} [:edit-text {:hint "Event name", :id ::name}] [:edit-text {:hint "Event location", :id ::location}] [:linear-layout {:orientation :horizontal} [:text-view {:hint "Event date", :id ::date}] [:button {:text "...", :on-click (fn [_] (show-picker (date-picker)))}]] [:button {:text "+ Event", :on-click (fn [_](add-event))}] [:text-view {:text @listing, :id ::listing}]]) (defn get-elmt [elmt] (str (.getText (elmt (.getTag mylayout))))) (defn set-elmt [elmt s] (on-ui (.setText (elmt (.getTag mylayout)) s))) (defn update-ui [] (set-elmt ::listing @listing) (set-elmt ::location "") (set-elmt ::name "")) (defn add-event [] (swap! listing str (get-elmt ::date) " - " (get-elmt ::location) " - " (get-elmt ::name) "\n") (update-ui)) (defactivity org.stuff.events.MyActivity :on-create (fn [this bundle] (on-ui (set-content-view! myActivity (make-ui main-layout))) (on-ui (set-elmt ::listing @listing)))) (defn date-picker [] (proxy [DialogFragment DatePickerDialog$OnDateSetListener] [] (onCreateDialog [savedInstanceState] (let [c (Calendar/getInstance) year (.get c Calendar/YEAR) month (.get c Calendar/MONTH) day (.get c Calendar/DAY_OF_MONTH)] (DatePickerDialog. myActivity this year month day))) (onDateSet [view year month day] (on-ui (.setText (::date (.getTag mylayout)) (str year (format "%02d" (inc month)) (format "%02d" day))))))) (defn show-picker [dp] (. dp show (. myActivity getFragmentManager) "datePicker"))

Sorting and Formatting the Listing

Now that we have dates, we can sort our listing. Let's change ourmt-listingconstructor to return asorted-mapatom.

(defn mt-listing [] (atom (sorted-map)))

We will now make a huge change to theadd-eventfunction. Are you ready? Let's leave formatting out of this and only deal with updating our data structure. The keys to our map will be an integer representing the date. Each date should be able to hold multiple events, so the value of the key will be a vector of location and name vectors.

(defn add-event [] (let [date-key (try (read-string (get-elmt ::date)) (catch RuntimeException e "Date string is empty!"))] (when (number? date-key) (if (some #{date-key} (keys @listing)) ; add to existing date (swap! listing assoc date-key (conj (@listing date-key) [(get-elmt ::location) (get-elmt ::name)])) ; add new date (swap! listing assoc date-key [[(get-elmt ::location) (get-elmt ::name)]])) (update-ui))))

Since our listingatomno longer references a string, we need to format our map. Since our data structure contains a vector of vectors, we will implement this using nested loops. To prevent this code from looking like a monstrosity, let's split it into two functions: one to loop over the dates and another to loop over the events within each date. Here we go.

(defn format-events [e] ; loop through events within dates (loop [events e ret "" loop0 true] (if-not events ret (let [loc (first (first events)) name (second (first events))] (recur (next events) (if loop0 (str ret loc " - " name "\n") ;NOTE: hard coding whitespace below (str ret "                      " loc " - " name "\n")) false))))) (defn format-listing [lst] ; loop through dates (loop [keyval (seq lst) ret ""] (if-not keyval ret (let [date (first (first keyval))] (recur (next keyval) (str ret date " - " ; loop through events within dates (format-events (second (first keyval)))))))))

Let's replace all occurrences in our code that assumes@listingto be a string with(format-listing @listing). In our layout:

(def main-layout [:linear-layout {:orientation :vertical, :id-holder :true, :def `mylayout} [:edit-text {:hint "Event name", :id ::name}] [:edit-text {:hint "Event location", :id ::location}] [:linear-layout {:orientation :horizontal} [:text-view {:hint "Event date", :id ::date}] [:button {:text "...", :on-click (fn [_] (show-picker (date-picker)))}]] [:button {:text "+ Event", :on-click (fn [_](add-event))}] [:text-view {:text (format-listing @listing), :id ::listing}]])

And inupdate-ui:

(defn update-ui [] (set-elmt ::listing (format-listing @listing)) (set-elmt ::location "") (set-elmt ::name "") (set-elmt ::date ""))

Succinctness is Power

Here is the source code so far:

(ns org.stuff.events.main (:use [neko.activity :only [defactivity set-content-view!]] [neko.threading :only [on-ui]] [neko.ui :only [make-ui]] [neko.application :only [defapplication]]) (:import (java.util Calendar) (android.app Activity) (android.app DatePickerDialog DatePickerDialog$OnDateSetListener) (android.app DialogFragment))) (declare android.widget.LinearLayout mylayout) (declare add-event) (declare date-picker) (declare show-picker) (defn mt-listing [] (atom (sorted-map))) (def listing (mt-listing)) (defn format-events [e] ; loop through events within dates (loop [events e ret "" loop0 true] (if-not events ret (let [loc (first (first events)) name (second (first events))] (recur (next events) (if loop0 (str ret loc " - " name "\n") ;NOTE: hard coding whitespace below (str ret "                      " loc " - " name "\n")) false))))) (defn format-listing [lst] ; loop through dates (loop [keyval (seq lst) ret ""] (if-not keyval ret (let [date (first (first keyval))] (recur (next keyval) (str ret date " - " ; loop through events within dates (format-events (second (first keyval))))))))) (def main-layout [:linear-layout {:orientation :vertical, :id-holder :true, :def `mylayout} [:edit-text {:hint "Event name", :id ::name}] [:edit-text {:hint "Event location", :id ::location}] [:linear-layout {:orientation :horizontal} [:text-view {:hint "Event date", :id ::date}] [:button {:text "...", :on-click (fn [_] (show-picker (date-picker)))}]] [:button {:text "+ Event", :on-click (fn [_](add-event))}] [:text-view {:text (format-listing @listing), :id ::listing}]]) (defn get-elmt [elmt] (str (.getText (elmt (.getTag mylayout))))) (defn set-elmt [elmt s] (on-ui (.setText (elmt (.getTag mylayout)) s))) (defn update-ui [] (set-elmt ::listing (format-listing @listing)) (set-elmt ::location "") (set-elmt ::name "") (set-elmt ::date "")) (defn add-event [] (let [date-key (try (read-string (get-elmt ::date)) (catch RuntimeException e "Date string is empty!"))] (when (number? date-key) (if (some #{date-key} (keys @listing)) ; add to existing date (swap! listing assoc date-key (conj (@listing date-key) [(get-elmt ::location) (get-elmt ::name)])) ; add new date (swap! listing assoc date-key [[(get-elmt ::location) (get-elmt ::name)]])) (update-ui)))) (defactivity org.stuff.events.MyActivity :on-create (fn [this bundle] (on-ui (set-content-view! myActivity (make-ui main-layout))) (on-ui (set-elmt ::listing (format-listing @listing))))) (defn date-picker [] (proxy [DialogFragment DatePickerDialog$OnDateSetListener] [] (onCreateDialog [savedInstanceState] (let [c (Calendar/getInstance) year (.get c Calendar/YEAR) month (.get c Calendar/MONTH) day (.get c Calendar/DAY_OF_MONTH)] (DatePickerDialog. myActivity this year month day))) (onDateSet [view year month day] (on-ui (.setText (::date (.getTag mylayout)) (str year (format "%02d" (inc month)) (format "%02d" day))))))) (defn show-picker [dp] (. dp show (. myActivity getFragmentManager) "datePicker"))

This is a bit over 100 lines of code. When I first attempted to write this app using Java, I was well over 1000 lines and didn't even have all this functionality before I gave up. If succinctness really is power6, then I'm never looking back.

The source code and the entire project directory can be found onmy GitHub.

Suggestions and Tips

Hopefully this is enough to get you started developing Android apps in Clojure. I leave polishing the app as an exercise for you, dear reader. To make this app actually useful, you might want to make the sorted map data structure persistent using some sort ofcontent provider. It might also help to make the:text-viewfor the listing scrollable once you have a lot of events listed.

Debugging and logging

You will probably run into errors and even bugs with the tools.adbwill prove to be very valuable. In a spare terminal, run this command:

<sdk-path>/platform-tools/adb logcat

nekoprovidesloggingcapabilities. Add this to the:usedirective in thensform:

[neko.log :only [deflog]]

Add this line near the top of your source file:

(deflog "MyTAG")

Now you can write lines toadb logcatby calling something like:

(log-d "My log message")
I lost my REPL!

Given the instability of the current tools, runtime errors in the code, and other bugs, you will probably lose your connection to the REPL at some point during development. Follow these steps to get back into your groove. In the terminal:

lein do droid run, droid forward-port

And inemacs:M-x nrepl, local machine for 'Host', and '9999' for 'Port'. Now in the REPL, run:

(in-ns 'org.stuff.events.main)
Then evaluate thensform to get back into your running app's namespace. The important thing to know is that all the new code you wrote after your lastbuildhas to be evaluated at the REPL in order to get back to your previous state. Runninglein droid doallat the terminal at key points of development can save you some steps whenever you lose your REPL connection.

Conclusion

The tools available for Android development are still young. Needless to say, you will probably run into some issues and bugs along the way. If you are serious about pursuing this bleeding edge stuff, get connected with the maintainers of these tools. Alex Yakushev in particular has been very helpful and quick to respond to me personally as I was learning how to uselein-droid. Phil Hagelberg (a.k.a.technomancy) ofleinis also pretty responsive.

Feel free to get in touch andfollow me on Twitter.

Notes

1http://steve-yegge.blogspot.com/2007/12/codes-worst-enemy.html

2http://www.niemanlab.org/2011/11/in-praise-of-impractical-programming/

3If you don't yet know Clojure, I recommend Joy of Clojure and watching Rich Hickey's talks. Despite it's power and simplicity, I wouldn't recommend Clojure to novice programmers. One should probably be fluent in at least two or three other languages before taking on Clojure. If you really want to dive into the rabbit hole, I recommend SICP before jumping into Clojure. Learning the Clojure way is quite a journey and deserves it's own blog post.

4Use mutable state at your own judgment. Learn more about immutability, state, and identity here.

5For reference, this code was translated from thispage.

6http://www.paulgraham.com/power.html

Last edited by Kris Calabio,3 months ago


更多相关文章

  1. Android跳转intent简单教程
  2. Android 显示网页图片
  3. android 图片与byte数组间的转换
  4. android 图片灰度处理的处理
  5. 轮播网络图片加载适配
  6. Android WebView 图片自适应屏幕宽度
  7. android 通过滚动条改变图片显示
  8. android 图片水平显示,类Gallery效果

随机推荐

  1. Android成长(三)——页面布局
  2. react-native-s-alipay -- React Native
  3. Android事件分发机制全解析
  4. Android 9.0 Launcher Workspace 加载
  5. Android的应用程序框架
  6. 高通平台android9.0设置开机默认横屏显示
  7. Android development - 'missing theme e
  8. Android公共库(缓存 下拉ListView 下载管
  9. Android TransactionTooLargeException
  10. Android中自定义ViewGroup实现表格展示学