Android 開發工具進化史
Rick(台灣大哥大-MyMusic)
開開心心的導入,安安穩穩的加班。
Outline
• Android Architecture
• AndroidX
• Jetpack
• Database
• Image Process
• Http Connection
• Dependency Injection
• ReactiveX
• Language
Android Architecture
• No Architecture
• MVC(Model-View-Controller)
• MVP(Model-View-Presenter)
• MVVM(Model-View-ViewModel)
No Architecture
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//100000 lines
}
No Architecture
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
try{
//100000 lines
} catch (exception: Exception){
//handle
}
}
No Architecture
架構軟體如同蓋教堂,完成之後就可以開始祈禱惹。
- Sam Redwine
MVC
MVP
MVVM
MVC | MVP | MVVM | |
---|---|---|---|
Google daddy support | Yes | No | No |
Learning curve | Easy | Normal | Normal |
Testability | Hard | Easy | Easy |
Lifecycle-aware | Hard | Hard | Easy |
MVC vs MVP vs MVVM
What is AndroidX
AndroidX is a new library refactored from the Support Library.
AndroidX means Android extension
docx
Android KTX
Rxjava
...
android.useAndroidX:
When set to true, the Android plugin uses the appropriate AndroidX library instead of a Support Library. The flag is false by default if it is not specified.
android.enableJetifier:
When set to true, the Android plugin automatically migrates existing third-party libraries to use AndroidX by rewriting their binaries. The flag is false by default if it is not specified.
Android Jetpack
Database
-
SQLite
-
GreenDao
-
ORMLite
-
Realm
-
Room[Google support]
SQLite
public static final String TABLE_NAME = "item";
public static final String KEY_ID = "_id";
public static final String DATETIME_COLUMN = "datetime";
public static final String COLOR_COLUMN = "color";
public static final String CREATE_TABLE =
"CREATE TABLE " + TABLE_NAME + " (" +
KEY_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
DATETIME_COLUMN + " INTEGER NOT NULL)";
declare
public List<Item> getAll() {
List<Item> result = new ArrayList<>();
Cursor cursor = db.query(
TABLE_NAME, null, null, null, null, null, null, null);
while (cursor.moveToNext()) {
result.add(getRecord(cursor));
}
cursor.close();
return result;
}
public boolean delete(long id){
String where = KEY_ID + "=" + id;
return db.delete(TABLE_NAME, where , null) > 0;
}
public Item insert(Item item) {
ContentValues cv = new ContentValues();
cv.put(DATETIME_COLUMN, item.getDatetime());
cv.put(COLOR_COLUMN, item.getColor().parseColor());
long id = db.insert(TABLE_NAME, null, cv);
item.setId(id);
return item;
}
query
delete
insert
public boolean update(Item item) {
ContentValues cv = new ContentValues();
cv.put(DATETIME_COLUMN, item.getDatetime());
cv.put(COLOR_COLUMN, item.getColor().parseColor());
String where = KEY_ID + "=" + item.getId();
return db.update(TABLE_NAME, cv, where, null) > 0;
}
update
GreenDao
@Entity
public class Student {
@Id(autoincrement = true)
private Long id;
@NonNull
private String name;
}
declare
public List<Student> getAllStudent() {
return studentDao.loadAll();
}
public boolean delete(Student student){
return studentDao.delete(student);
}
public long insert(Student student) {
long id = studentDao.insert(student);
}
query
delete
insert
public boolean update(Student student) {
studentDao.update(student);
}
update
ORMLite
@DatabaseTable(tableName = "User")
public class User {
@DatabaseField(generatedId = true) private int id;
@DatabaseField private String name;
@DatabaseField private int sort;
@ForeignCollectionField private ForeignCollection<Group> groups;
}
declare
public List<T> getAll() {
try {
return dao.queryBuilder().orderBy("id", false).query();
} catch (SQLException e) {
e.printStackTrace();
return Collections.emptyList();
}
}
public int deleteById(ID primaryKey) {
int id;
try {
id = dao.deleteById(primaryKey);
} catch (SQLException e) {
e.printStackTrace();
id = -1;
}
return id;
}
public int insert(T item) {
int id;
try {
id = dao.create(item);
} catch (SQLException e) {
e.printStackTrace();
id = -1;
}
return id;
}
query
delete
insert
public int update(T item) {
int id;
try {
id = dao.update(item);
} catch (SQLException e) {
e.printStackTrace();
id = -1;
}
return id;
}
update
Realm
public class User extends RealmObject {
@PrimaryKey
private String name;
private int age;
private RealmList<User> friends;
@Ignore
private int sessionId;
}
declare
public RealmResults<User> getAll() {
RealmResults<User> result = mRealm.where(User.class).findAll();
}
public int delete(int userId) {
realm.executeTransaction(new Realm.Transaction(){
@Override
public void execute(Realm realm){
RealmResults<User> users = realm.where(User.class).equalTo(Message.USER_ID, userId).findAll();
users.clear();
}
}
}
public int insert(final String name, final int age) {
realm.executeTransaction(new Realm.Transaction(){
@Override
public void execute(Realm realm){
User user = realm.createObject(User.class);
user.setName(name);
user.setAge(age);
}
}
}
query
delete
insert
public int update(String name, int age) {
realm.executeTransaction(new Realm.Transaction(){
@Override
public void execute(Realm realm){
User user = realm.where(User.class).findFirst();
user.setName(name);
user.setAge(age);
}
}
}
update
Room
@Entity
public class User{
@PrimaryKey
public int uid;
@ColumnInfo(name = "first_name")
public String firstName;
@ColumnInfo(name = "last_name")
public String lastName;
}
Context context = ApplicationProvider.getApplicationContext();
db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
userDao = db.getUserDao();
User user = TestUtil.createUser(3);
user.setName("george");
userDao.insert(user);
@Dao
public interface UserDao {
@Query("SELECT * FROM user")
List<User> getAll();
@Query("SELECT * FROM user WHERE uid IN (:userIds)")
List<User> loadAllByIds(int[] userIds);
@Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
"last_name LIKE :last LIMIT 1")
User findByName(String first, String last);
@Insert
void insertAll(User... users);
@Delete
void delete(User user);
}
我們為什麼選擇 Room?
它有Google把拔
Querying multiple tables
@Dao
interface MyDao {
@Query(
"SELECT user.name AS userName, pet.name AS petName " +
"FROM user, pet " +
"WHERE user.id = pet.user_id"
)
fun loadUserAndPetNames(): LiveData<List<UserPet>>
// You can also define this class in a separate file.
data class UserPet(val userName: String?, val petName: String?)
}
Image Process
-
Universal Image Loader
-
Picasso[Square]
-
Fresco[Facebook]
-
Glide[This is not an official Google product.]
Universal Image Loader
Really have no time for development... so I stop project maintaining since Nov 27 :(
UIL [27.11.2011 - 27.11.2015]
Thanks to all developers for your support :)
Picasso vs Fresco vs Glide
Picasso | Fresco | Glide | |
---|---|---|---|
Support gif | No | Yes | Yes |
Circle image | No | Yes | Yes(4.0+) |
Method size | 850 | 5,800 | 2,800 |
Support webP | Yes | Yes | Yes |
Android lifecycle | No | No | Yes |
Cache | Large | Normal | Small |
Container | ImageView | DraweeView | ImageView |
Http Connection
-
HttpClient
-
HttpURLConnection
-
AsyncTask
-
OkHttp
-
Volley
-
Retrofit
HttpClient vs HttpURLConnection
For Gingerbread and better, HttpURLConnection is the best choice. New applications should use HttpURLConnection.
AsyncTask + HttpURLConnection
class URLTask extends AsyncTask<String, Integer, String> {
@Override
protected void onPreExecute(){
}
@Override
protected String doInBackground(String... params) {
try {
URL url = new URL(params[0]);
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setDoInput(true);
urlConnection.setDoOutput(true);
urlConnection.setRequestProperty("Content-Type", "application/json");
urlConnection.setRequestMethod("POST");
urlConnection.setRequestProperty("Authorization", "someAuthString");
if (this.postData != null) {
OutputStreamWriter writer = new OutputStreamWriter(urlConnection.getOutputStream());
writer.write(postData.toString());
writer.flush();
}
int statusCode = urlConnection.getResponseCode();
if (statusCode == 200) {
InputStream inputStream = new BufferedInputStream(urlConnection.getInputStream());
String response = convertInputStreamToString(inputStream);
} else {
// Status code is not 200
}
} catch (Exception e) {
Log.d(TAG, e.getLocalizedMessage());
}
}
@Override
protected void onProgressUpdate(Integer... values) {
}
@Override
protected void onPostExecute(String result) {
}
}
Volley
val textView = findViewById<TextView>(R.id.text)
// ...
// Instantiate the RequestQueue.
val queue = Volley.newRequestQueue(this)
val url = "http://www.google.com"
// Request a string response from the provided URL.
val stringRequest = StringRequest(Request.Method.GET, url,
Response.Listener<String> { response ->
// Display the first 500 characters of the response string.
textView.text = "Response is: ${response.substring(0, 500)}"
},
Response.ErrorListener { textView.text = "That didn't work!" })
// Add the request to the RequestQueue.
queue.add(stringRequest)
OkHttp
public static final MediaType JSON
= MediaType.get("application/json; charset=utf-8");
OkHttpClient client = new OkHttpClient();
String post(String url, String json) throws IOException {
RequestBody body = RequestBody.create(JSON, json);
Request request = new Request.Builder()
.url(url)
.post(body)
.build();
try (Response response = client.newCall(request).execute()) {
return response.body().string();
}
}
AsyncTask | Volley | OkHttp | |
---|---|---|---|
readability | No | Yes | Yes |
AsyncTask vs Volley vs OkHttp
HttpURLConnection
try{
HttpURLConnection.setFollowRedirects(false);
HttpURLConnection con = (HttpURLConnection) new URL(url).openConection();
con.setRequestMethod("GET");
con.setConnectTimeout(5000);
} catch(SocketTimeoutException e){
e.printStackTrace();
} catch(IOException e){
e.printStackTrace();
}
Volley
volleyRequest.setRetryPolicy(new DefaultRetryPolicy(
MY_SOCKET_TIMEOUT_MS,
DefaultRetryPolicy.DEFAULT_MAX_RETRIES,
DefaultRetryPolicy.DEFAULT_BACKOFF_MULTI));
OkHttp
OkHttpClient.Builder()
.retryOnConnectionFailure(true)
.connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
.readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
.writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)
.followRedirects(false)
.addIntercepteor(new RetryInterceptor())
.addIntercepteor(new LoggerInterceptor())
.build();
OkHttp Interceptor
HttpURLConnection vs Volley vs OkHttp
HttpURLConnection | Volley | OkHttp | |
---|---|---|---|
Settings | simple | simple | various choices |
Retrofit
@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId);
@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId, @Query("sort") String sort);
@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId, @QueryMap Map<String, String> options);
@POST("users/new")
Call<User> createUser(@Body User user);
class User{
@SerializedName("user_name")
private String userName;
@SerializedName("user_age")
private int userAge;
}
HttpURLConnection vs Volley vs OkHttp
HttpURLConnection | Volley | Retrofit | |
---|---|---|---|
annotations to describe | No | No | Yes |
AsyncTask vs Volley vs Retrofit
One Discussion | Dashboard | 25 Discussions | |
---|---|---|---|
AsyncTask | 941ms | 4, 539ms | 13, 957ms |
Volley | 560ms | 2, 202ms | 4, 275ms |
Retrofit | 312ms | 889ms | 1, 059ms |
AsyncTask vs Volley vs Retrofit
AsyncTask + HttpURLConnection |
Volley + HttpClient | Retrofit + OkHttp | |
---|---|---|---|
readability | Hard | Easy | Easy |
Interceptor | No | No | Yes |
Annotation describe | No | No | Yes |
Speed | Slow | Normal | Fast |
Class Singer{
public Singer(){
Album album = new Album();
//...
}
}
What is dependency injection?
- new is evil
- high coupling
- hard to replace
- hard to test
Class Singer{
private Album album;
public Singer(Album album){
this.album = album;
//...
}
}
地方的物件需要注射
- Reusability
- Testing
- Maintainability
Dependency Injection
-
Dagger2
-
Koin2
Dagger2
Dagger2
•Hard to learn
•Stable
•Flexible
•Powerful
•Fast in rumtime
•For Java and Kotlin
Koin
Koin
- Performance : shorturl.at/pBEO5, shorturl.at/bgklX
- For Kotlin
- Easy to learn
- Error in runtime
- No annotation
該選擇哪一個?
小朋友才選擇
What is callback hell?
new Thread(new Runnable(){
@Override
public void run(){
super.run();
List<Album> albums = getAlbumFromApi();
for(Album album : albums){
List<Song> songs = album.getSongList();
for(Song song : songs){
if(song.playTime >= 300){
tmpList.add(song);
runOnUiThread(new Runnable(){
list.songName.setText(song.getName());
})
}
}
}
}
})
RxJava
Observable.fromIterable(getAlbumFormApi())
.flatMap(new Function<Album, ObservalbeSource<Song>>(){
@Override
public ObservaleSource<Song> apply(Album album) throws Exception{
return Observable.fromIterable(album.getSongs());
}
})
.filter(new Preficate<Song>(){
@Oberride
public boolean test(@NonNull Song song) throws Exception{
return song.Time > 300;
}
})
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidScheduler.mainThread())
.subscribe(new Consumer<Song>(){
@Override
public void accept(Song song) throws Exception{
list.add(song);
}
})
RxJava(use Java Lambda)
Observable.fromIterable(getAlbumFormApi())
.flatMap((Function<Album, ObservalbeSource<Song>>) album->Observable.fromIterable(album.getSongs()))
.filter(s-> s.Time > 300)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidScheduler.mainThread())
.subscribe(s->list.add(song));
RxJava(use Kotlin)
Observable.fromIterable(getAlbumFormApi())
.flatMap<Song>{Observable.fromIterable<Song>{album.getSongs()}}
.filter{it.Time > 300}
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidScheduler.mainThread())
.subscribe{list.add(it)}
Why we choice Rxjava
- Fix callback hell
- Asynchronous handle
- Clean code
Language
-
Java
-
Kotlin
Java
Observable.fromIterable(getAlbumFormApi())
.flatMap(new Function<Album, ObservalbeSource<Song>>(){
@Override
public ObservaleSource<Song> apply(Album album) throws Exception{
return Observable.fromIterable(album.getSongs());
}
})
.filter(new Preficate<Song>(){
@Oberride
public boolean test(@NonNull Song song) throws Exception{
return song.Time > 300;
}
})
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidScheduler.mainThread())
.subscribe(new Consumer<Song>(){
@Override
public void accept(Song song) throws Exception{
list.add(song);
}
})
Kotlin
Observable.fromIterable(getAlbumFormApi())
.flatMap<Song>{Observable.fromIterable<Song>{album.getSongs()}}
.filter{it.Time > 300}
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidScheduler.mainThread())
.subscribe{list.add(it)}
Kotlin is best choice
Conclusion
Item name | Better choice |
---|---|
Architecture | MVVM(Google) |
Http Connection | Retrofit |
Database | Room(Google) |
Image Process | Glide |
Dependency Injection | Koin |
ReactiveX | RxJava |
Language | Kotlin(Google) |
Q & A
Thank you.
Android 開發工具進化史
By givemepass
Android 開發工具進化史
描述 Android 開發中所需的工具以及一些歷史
- 1,446