Hướng dẫn RxJava: Tìm hiểu về Observables và Observers

Hi anh em, tiếp tục với Seri về RxJava nhé.

bài trước, mình đã giới thiệu sơ qua về Reactive Programming là gì, cũng như tầm quan trọng của việc nắm bắt và hiểu biết về nó. Bài này chúng ta sẽ bắt đầu đào sâu hơn về Reactive programming, mà cụ thể ở đây là RxJava.

Trước hết chúng ta sẽ tìm hiểu về 2 khái niệm quan trọng bậc nhất trong Rx: ObservablesObservers.

[toc]

Observables và Observers? Gì đây ta?

Trong RxJava có 2 khái niệm mà ta bắt buộc phải biết và nắm rõ, đó là Observables và Observers. Chúng như là 1 đôi bạn không thể tách rời trong thế giới Rx.

Observables

Observable là những thứ có thể phát ra dữ liệu, hay có thể gọi nôm na là nguồn phát dữ liệu. Nó có nhiệm vụ thông báo cho những gì đang theo dõi nó (mà sau này chúng ta sẽ biết: Observer) mỗi khi có dữ liệu mới nhất. Ta gọi việc “thông báo” này là “EMIT”

Observers

Song song cùng khái niệm Observable, ta có Observer.

Observer sẽ là những thứ theo dõi Observable. Có nghĩa là mỗi khi Observable “EMIT” dữ liệu, Observer sẽ nhận được dữ liệu đó và xử lý chúng. Ta gọi sự “theo dõi” này là “SUBSCRIBE”.

Observable và Observer đầu tiên của bạn

Mình sẽ mô tả các kiến thức của seri này thông qua 1 dự án Android. Tất nhiên không chỉ có Android mới có thể sử dụng RxJava. Nhưng mình thích nên mình sẽ dùng nó để mô tả các ví dụ 😀

Chuẩn bị: Import thư viện RxJava 2

Mình sẽ implement thư viện RxJava trong file build.gradle như sau:

implementation 'io.reactivex.rxjava2:rxjava:2.2.2'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'

Ok, mọi chuẩn bị đã xong xuôi, giờ mình sẽ tạo 1 Observable.

Observable đầu tiên

Có nhiều cách để tạo ra các Observable. Đây là 1 ví dụ:

Observable<String> observable = new ObservableCreate<String>(new ObservableOnSubscribe<String>() {
            @Override
            public void subscribe(@NonNull ObservableEmitter<String> emitter) throws Exception {
                emitter.onNext("cungdev.com");
                emitter.onNext("hello anh em");
                emitter.onComplete();
            }
});

Ở đây mình đã tạo 1 observable bằng interface ObservableCreate. Có 3 điều ta cần chú ý.

  1. Observable mình tạo ra có kiểu Observable<String>. Tức là observable sẽ phát ra (Emit)  các dữ liệu có dạng String.

  2. Để theo dõi (Subscribe) Observable<String> thì ta cũng cần 1 Observer chuyên để xử lý dữ liệu dạng String. Như bạn thấy ở trên, ta đã dùng 1 ObservableEmitter<String> để xử lý.

  3. Interface ObservableCreate mà mình dùng để tạo ra Observable<String> có 1 phương thức subscribe(@NonNull ObservableEmitter<String> emitter)

    Đây là phương thức được gọi mỗi khi observable của mình có 1 Observer theo dõi. Như bạn thấy, khi có 1 Observer subscribe Observable này, Observable sẽ Emit 2 giá trị: “cungdev.com” và “hello anh em” và sẽ gọi phương thức emitter.onComplete(); Mình sẽ giải thích phương thức này sau.

 

Bây giờ, nếu bạn chạy đoạn code này lên, thử đoán xem chuyện gì sẽ xảy ra. Câu trả lời là sẽ chả có gì xảy ra cả 😀 Nguyên nhân là vì chỉ khi có 1 Observer nào đó SUBSCRIBE Observable ta vừa tạo, thì Observable mới bắt đầu EMIT các giá trị.

Ở đây ta chưa tạo ra Observer nào để SUBSCRIBE Observable, vì vậy không có gì xảy ra. Bây giờ ta sẽ bắt đầu đi tạo Observer.

Observer đầu tiên

Mình sẽ tạo ra Observer như sau:

Observer<String> observer = new Observer<String>() {
            @Override
            public void onSubscribe(Disposable d) {
                System.out.println("onSubscribe");
            }

            @Override
            public void onNext(String o) {
                System.out.println("onNext " + o);
            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onComplete() {
                System.out.println("onComplete");
            }
};

Sau khi đã tạo xong Observer, mình sẽ cho nó SUBSCRIBE Observable ở trên:

observable.subscribe(observer);

Lúc này, khi chạy ứng dụng lên mình sẽ thấy xuất hiện các dòng log:

System.out: onSubscribe
System.out: onNext cungdev.com
System.out: onNext hello anh em
System.out: onComplete

Giải thích

Như mình đã nói ở phần tạo Observable, trong ví dụ này, mỗi khi có 1 Observer SUBSCRIBE Observable, nó sẽ EMIT 2 String “cungdev.com” và “hello anh em”, sau đó gọi đến phương thức onComplete.

Observable EMIT dữ liệu bằng cách nào? Bằng cách gọi phương thức onNext của Observer.

Observer mình tạo ra đã xử lý để tiếp nhận và log ra các giá trị phát ra từ Observable. Hãy nhìn vào phương thức onNext:

@Override
public void onNext(String o) {
   System.out.println("onNext " + o);
}

Khi Obervable không muốn EMIT thêm các giá trị đến 1 Observer nào đó, nó sẽ gọi phương thức onComplete của Observer đó

emitter.onComplete();

Và phương thức onComplete được chúng ta định nghĩa khi tạo Observer như sau:

@Override
public void onComplete() {
    System.out.println("onComplete");
}

Kết quả là chúng ta đã có được những dòng log ở trên 😀

Tổng kết

Observable và Observer là 2 khái niệm song hành cùng nhau trong thế giới Rx.

Một observer subscribe 1 observable để tiếp nhận dữ liệu. Còn 1 observable gửi dữ liệu đến các observer của nó bằng cách gọi các phương thức của Observer.

Các phương thức mà Observer có là:

  1. onNext – Khi có 1 dữ liệu phát ra từ Observable
  2. onComplete – Khi không còn dữ liệu nào phát ra từ Observable
  3. onError – Khi có lỗi xảy ra và Observable không thể phát ra dữ liệu nữa. Sẽ có 1 bài riêng để nói về cái này.
  4. onSubscribe – Khi observer bắt đầu SUBSCRIBE observable.

OK, mình nghĩ chừng này thông tin là đủ để các bạn hiểu sơ bộ về Observables và Observers rồi. Hẹn gặp lại trong các bài tiếp theo.

Hướng dẫn RxJava: Giới thiệu

Chào mọi người, hôm nay mình sẽ quyết định viết 1 seri về chủ đề Reactive Programming, cụ thể ở đây là RxJava. Đây là thư viện mà có lẽ rất rất nhiều dự án Android đã đang và sẽ sử dụng (mặc cho sự phát triển và cải thiện không ngừng của Kotlin Coroutine, Flow :D). Vì vậy mình quyết định viết seri này. Mục tiêu hướng đến của seri này là những anh em dev chưa tiếp cận hoặc mới tiếp cận và muốn tìm hiểu thêm với Reactive Programming, cụ thể là RxJava. OK. Start thôi.

[toc]

Vậy Reactive Programming là cái cóc khô gì?

Reactive Programming là 1 mô hình lập trình liên quan đến luồng dữ liệu và sự thông báo khi có sự thay đổi về dữ liệu. WTF? Nói cái gì thế??? Nghe không hiểu mô tê gì.

Để dễ hiểu hơn thì hãy cùng mình xét một bài toán đơn giản: Cộng 2 số nguyên.

Với cách lập trình truyền thống, bài toán sẽ được giải như sau:

void normalAddition() {
    int a = 1;
    int b = 2;
    int sum = a + b;
    Log.d(TAG, "Ket qua: "+ sum);
}

Khi chạy hàm này thì kết quả sẽ thu được là:

Ket qua: 3.

OK không vấn đề gì.

Giờ ta hãy thay đổi hàm normalAddition này một chút, ta sẽ cập nhật giá trị cho biến A và kiểm tra lại kết qủa của biến sum:

void normalAddition() {
    int a = 1;
    int b = 2;
    int sum = a + b;
    Log.d(TAG, "Ket qua: "+ sum);
    a = 3;
    Log.d(TAG, "Ket qua: "+ sum);
}

Khi chạy lại thì được kết quả:

Ket qua: 3

Ket qua: 3

Như vậy ta thấy vấn đề là sum không được cập nhật giá trị mặc dù a đã được cập nhật.

Còn đối với Reactive programming, khi biến a thay đổi thì biến sum cũng sẽ được thông báo về sự thay đổi này, và có hành động xử lý, dĩ nhiên là update lại giá trị rồi :D.

Hãy hình dung như ở trang tính excel, khi bạn update giá trị của a, thì giá trị của sum cũng sẽ được tự động update theo vậy.

Nói tóm lại Reactive programming là mô hình lập trình mà trong đó, mọi sự thay đổi của 1 đối tượng đều được thông báo đến những đối tượng đang theo dõi nó. Như ở ví dụ trên là sum đang theo dõi ab.

 

RXJava

RxJava là một thư viện hỗ trợ Reactive programming viết riêng cho Java. Rõ ràng rồi, chúng ta đâu cần chế tạo lại cái bánh xe, cũng đủ thời gian để implement từng đối tượng, cho chúng theo dõi nhau theo 1 cách thủ công được. Vậy ta cần 1 thư viện hỗ trợ ta điều đó, thư viện này là RxJava.

Trong seri này ta sẽ tìm hiểu các tính năng hữu ích nhất của RxJava để sử dụng trong quá trình phát triển. Mình cũng sẽ đưa ra từng use case cho từng tính năng để các bạn dễ hình dung.

Bài viết tiếp theo mình sẽ giới thiệu về 2 khái niệm cơ sở của RxJava và cách chúng ta làm việc với chúng.

Cảm ơn đã đọc bài viết. Hẹn gặp lại.

[toc]

Giới thiệu về Dagger 2,cách sử dụng Dependency Injection in Android (Phần 2)

Chào các bạn, đây là bài thứ 2 trong loạt bài viết hướng dẫn về Dependency Injection và sử dụng thư viện Dagger 2 trong Android. Nếu bạn chưa đọc qua phần 1, bạn có thể đọc ở đây.

Phần này mình sẽ xây dựng 1 ứng dụng demo để mô tả cách thức làm việc với Dagger 2 trong Android. 1 ứng dụng hết sức cơ bản thôi để giúp các bạn hình dung được cách làm. Đây là video demo của ứng dụng:

Đây là ứng dụng có chức năng load dữ liệu về các Hotgirl từ trong database SQLite ra và hiển thị ra RecyclerView. Nó còn có thêm chức năng load 1 đoạn accessToken từ SharePreference và hiển thị lên Toolbar của ứng dụng. Just demo, đơn giản đúng không?

Source của ứng dụng: https://github.com/nanashi1111/DaggerExample

[toc]

Bắt đầu

Đầu tiên, bạn hãy nhìn vào cấu trúc của dự án.

Các thành phần cốt lõi của ứng dụng bao gồm:

DataManager: Lớp cung cấp các phương thức truy cập vào dữ liệu trong ứng dụng. Dữ liệu có thể lấy từ SQLite Database hoặc SharedPreference.

DbHelper: Lớp cung cấp các phương thức truy cập vào SQLite Database, lớp này được sử dụng bởi DataManager.

SharedPrefsHelper: Lớp cung cấp các phương thức làm việc với SharedPreference. Lớp này cũng được sử dụng bởi DataManager.

Hotgirl: Lớp model của ứng dụng, để lưu trữ thông tin của các hotgirl <3

Bước 1: thêm thư viện Dagger 2 vào dự án.

Thêm các câu lệnh sau vào file build.gradle (ở app module) để import thư viện Dagger 2.

//Dagger
implementation "com.google.dagger:dagger:2.8"
annotationProcessor "com.google.dagger:dagger-compiler:2.8"
compileOnly 'javax.annotation:jsr250-api:1.0'
implementation 'javax.inject:javax.inject:1'

Bước 2: Xây dựng class model Hotgirl

Bao giờ cũng vậy, ta cần xây dựng class Model để lưu trữ dữ liệu và làm việc với các thành phần khác của ứng dụng như database, activity…

public class Hotgirl {

    String name;
    String avatar;

    public Hotgirl(String name, String avatar) {
        this.name = name;
        this.avatar = avatar;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAvatar() {
        return avatar;
    }

    public void setAvatar(String avatar) {
        this.avatar = avatar;
    }
}

Bước 3: Tạo ra các custom annotation

Ta sẽ tạo ra các annotation sau: ActivityContextApplicationContextDatabaseInfo , PerActivity

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivityContext {
}

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface ApplicationContext {
}

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface DatabaseInfo {
}

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface PerActivity {
}

Tại sao phải tạo ra các annotation này?

Annotation @Qualifier nằm trong package javax.inject. Nó được sử dụng để phân biệt các đối tượng mà Dagger sẽ phân phối cho các lớp Dependency consumer. Ví dụ: 1 class có thể yêu cầu cung cấp cả ApplicationContextActivityContext. Trong khi 2 đối tượng này đều là thể hiện của lớp Context. Vậy ta cần 1 cái gì đó để giúp Dagger 2 phân biệt được 2 đối tượng này. Thứ ta cần chính là các @Qualifier annotation. Nó giúp Dagger 2 phân biệt được các đối tượng thuộc cùng 1 kiểu dữ liệu (trong trường hợp này là Context).

Annotation @DatabseInfo để cung cấp thông tin về các thuộc tính để khởi tạo DBHelper. Các thuộc tính này là name và version.

Annotation @Scope để chỉ ra vùng tồn tại của các đối tượng được dagger cung cấp. Khi 1 class được Inject các dependency bởi Dagger, và các dependency đó được chỉ định @Scope, thì mỗi thể hiện của class đó sẽ được cung cấp các dependency khác nhau, độc lập và tồn tại trong vòng đời của class đó.

Bước 4, tạo DBHelper

Tạo ra 1 class DBHelper, class này sẽ đảm nhiệm mọi công việc liên quan đến database SQLite, thêm, xoá dữ liệu, clear db,…

@Singleton
public class DBHelper extends SQLiteOpenHelper {

    public static final String USER_TABLE_NAME = "hotgirls";
    public static final String USER_COLUMN_USER_ID = "id";
    public static final String USER_COLUMN_USER_NAME = "girl_name";
    public static final String USER_COLUMN_USER_AVATAR = "girl_avt";

    @Inject
    public DBHelper(@ApplicationContext Context context,
                    @DatabaseInfo String dbName,
                    @DatabaseInfo Integer version) {
        super(context, dbName, null, version);
    }


    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        tableCreateStatements(sqLiteDatabase);
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
        sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + USER_TABLE_NAME);
        onCreate(sqLiteDatabase);
    }

    private void tableCreateStatements(SQLiteDatabase sqLiteDatabase) {
        try {
            sqLiteDatabase.execSQL(
                    "CREATE TABLE IF NOT EXISTS "
                            + USER_TABLE_NAME + "("
                            + USER_COLUMN_USER_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
                            + USER_COLUMN_USER_NAME + " VARCHAR(20), "
                            + USER_COLUMN_USER_AVATAR + " VARCHAR(50))"
            );

        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    protected Long insertUser(Hotgirl girl) {
        try {
            SQLiteDatabase db = this.getWritableDatabase();
            ContentValues contentValues = new ContentValues();
            contentValues.put(USER_COLUMN_USER_NAME, girl.getName());
            contentValues.put(USER_COLUMN_USER_AVATAR, girl.getAvatar());
            return db.insert(USER_TABLE_NAME, null, contentValues);
        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        }
    }

    protected List<Hotgirl> getAllGirl() throws Resources.NotFoundException, NullPointerException {
        Cursor cursor = null;
        List<Hotgirl> listGirl = new ArrayList<>();
        try {
            SQLiteDatabase db = this.getReadableDatabase();
            cursor = db.rawQuery("select * from " + USER_TABLE_NAME, null);

            if (cursor.getCount() > 0) {
                cursor.moveToFirst();
                do {
                    Hotgirl girl = new Hotgirl(cursor.getString(cursor.getColumnIndex(USER_COLUMN_USER_NAME)), cursor.getString(cursor.getColumnIndex(USER_COLUMN_USER_AVATAR)));
                    listGirl.add(girl);
                } while (cursor.moveToNext());
            }
        } catch (NullPointerException e) {
            e.printStackTrace();
            throw e;
        } finally {
            if (cursor != null) {
                cursor.close();
            }
            return listGirl;
        }
    }

    protected void clearDatabase() {
        getWritableDatabase().execSQL("delete from " + USER_TABLE_NAME);
    }
}

Hãy chú ý vào các Annotation xuất hiện trong class này:

@Singleton đảm bảo cho đối tượng DBHelper được khởi tạo duy nhất trong vòng đời của ứng dụng. Tức là cho dù ta có @Inject DBHelper ở nhiều activity, thì các đối tượng đó đều là 1.

@Inject trước hàm khởi tạo DBHelper. Chỉ ra rằng lớp DBHelper sẽ được thêm vào Dependency Graph, có nghĩa là khi cần khởi tạo đối tượng DBHelper, Dagger sẽ tìm đến phương thức khởi tạo được gắn Annotation @Inject này.

Qualifier @ApplicationContext chỉ ra rằng đối tượng context cần để khởi tạo DBHelper là đối tượng ApplicationContext, (chứ không phải ActivityContext).

@DatabaseInfo chỉ ra các thông tin cần để khởi tạo DBHelper: ở đây là name và version.

Mình sẽ quay trở lại giải thích kỹ hơn các Annotation này khi làm việc với @Module.

Tất cả các phương thức còn lại chỉ liên quan đến các operation thêm sửa xoá dữ liệu. Mình sẽ không nói kỹ thêm ở đây.

Bước 5, tạo class SharedPrefsHelper

Lớp này để xử lý các operation liên quan đến SharedPreference.

public class SharedPrefsHelper {

    public static String PREF_KEY_ACCESS_TOKEN = "access-token";
    SharedPreferences mSharedPreferences;

    @Inject
    public SharedPrefsHelper(SharedPreferences sharedPreferences) {
        this.mSharedPreferences = sharedPreferences;
    }

    public void put(String key, String value) {
        mSharedPreferences.edit().putString(key, value).apply();
    }

    public void put(String key, int value) {
        mSharedPreferences.edit().putInt(key, value).apply();
    }

    public void put(String key, float value) {
        mSharedPreferences.edit().putFloat(key, value).apply();
    }

    public void put(String key, boolean value) {
        mSharedPreferences.edit().putBoolean(key, value).apply();
    }

    public String get(String key, String defaultValue) {
        return mSharedPreferences.getString(key, defaultValue);
    }

    public Integer get(String key, int defaultValue) {
        return mSharedPreferences.getInt(key, defaultValue);
    }

    public Float get(String key, float defaultValue) {
        return mSharedPreferences.getFloat(key, defaultValue);
    }

    public Boolean get(String key, boolean defaultValue) {
        return mSharedPreferences.getBoolean(key, defaultValue);
    }

    public void deleteSavedData(String key) {
        mSharedPreferences.edit().remove(key).apply();
    }
}

Cũng như lớp DBHelper, lớp này cũng được gắn thêm Annotation @Singleton để đảm bảo đối tượng của class này được khởi tạo 1 lần duy nhất trong vòng đời ứng dụng.

Annotation @Inject ở hàm khởi tạo cho phép thêm lớp này vào Dependency Graph và Dagger có thể sử dụng hàm khởi tạo này khi cần.

Vậy các lớp DBHelper, SharedPrefsHelper này được cung cấp như thế nào? Hãy cùng mình tìm hiểu.

Bước 6, DataManager

@Singleton
public class DataManager {

    DBHelper mDbHelper;
    SharedPrefsHelper mSharedPrefsHelper;

    @Inject
    public DataManager(DBHelper dbHelper, SharedPrefsHelper sharedPrefsHelper) {
        this.mDbHelper = dbHelper;
        this.mSharedPrefsHelper = sharedPrefsHelper;
    }

    public Long addHotGirl(Hotgirl girl) {
        return mDbHelper.insertUser(girl);
    }

    public List<Hotgirl> getAllGirl() throws Resources.NotFoundException, NullPointerException {
        return mDbHelper.getAllGirl();
    }

    public void clearDatabase() {
        mDbHelper.clearDatabase();
    }

    public void saveAccessToken(String accessToken) {
        mSharedPrefsHelper.put(SharedPrefsHelper.PREF_KEY_ACCESS_TOKEN, accessToken);
    }

    public String getAccessToken() {
        return mSharedPrefsHelper.get(SharedPrefsHelper.PREF_KEY_ACCESS_TOKEN, null);
    }
}

Phần quan trọng bắt đầu ở đây nhé. Lớp DataManager này sử dụng đối tượng của các lớp Context, DBHelper, SharedPrefsHelper. Hàm khởi tạo của lớp DataManager này cũng được gán Annotation @Inject, tức là nó cũng được gán vào Dependency Graph.

Khi Dagger cần khởi tạo DataManager, nó sẽ tìm trong Dependency Graph các class cần có trong hàm khởi tạo của lớp này. Ở đây là Context, SharePrefsHelper, và DBHelperSharePrefsHelper, và DBHelper đã được thêm vào graph thông qua Annotation @Inject nên Dagger sẽ tự động gọi hàm khởi tạo của 2 lớp này ra. Vậy còn @ApplicationContext Context context thì sao? Hãy cùng mình đi vào bước 7.

Bước 7, Application.

Ta viết 1 lớp App kế thừa Application để biểu diễn vòng đời của toàn ứng dụng như sau. Đây cũng chính là lớp để Dagger sử dụng cho việc cung cấp @ApplicationContext Context cho DataManager.

public class App extends Application {

    ApplicationComponent applicationComponent;

    @Inject
    DataManager dataManager;

    @Override
    public void onCreate() {
        super.onCreate();
        initApplicationComponent();
    }

    private void initApplicationComponent() {
        applicationComponent = DaggerApplicationComponent.builder().applicationModule(new ApplicationModule(this)).build();
        applicationComponent.inject(this);
    }

    public DataManager getDataManager() {
        return dataManager;
    }

    public ApplicationComponent getApplicationComponent() {
        return applicationComponent;
    }
}

Tiếp theo thêm class này vào file Manifest:

<application
    ...
    android:name=".App"
    ...
</application>

Lưu ý: lớp App này sử dụng DataManager thông qua Annotation @Inject. Phần thú vị tiếp theo nằm ở class ApplicationComponent. Ta sẽ tìm hiểu ở các bước sau.

Bước 8, Xây dựng module cung cấp Dependency (Provider)

Đây là bước rất quan trọng, ta sẽ xây dựng các module cung cấp các Dependency để sử dụng trong activity của ta. Đầu tiên là ApplicationModule.

@Module
public class ApplicationModule {

    App application;

    public ApplicationModule(App application) {
        this.application = application;
    }

    @Provides
    @ApplicationContext
    public Context provideApplicationContext() {
        return application;
    }

    @Provides
    @DatabaseInfo
    public String provideDBName() {
        return "DaggerExample-DB";
    }

    @Provides
    @DatabaseInfo
    public Integer provideDBVersion() {
        return 1;
    }

    @Provides
    public SharedPreferences provideSharedPreference() {
        return application.getSharedPreferences("DaggerExample-SharedPrefs", Context.MODE_PRIVATE);
    }
}

Như các bạn thấy module này chỉ là 1 class bình thường, ngoại trừ việc nó được gán Annotation @Module, và các phương thức của nó đều được gán Annotation @Provides. Mình sẽ giải thích vì sao lại viết các phương thức này.

Như mình đã viết ở bước 6, mình đã thêm class DataManager vào Dependency Graph. Tức là khi cần khởi tạo đối tượng DataManager, Dagger 2 sẽ tìm các thành phần cần thiết trong Dependency Graph để thực hiện hàm khởi tạo DataManager:

@Inject
public DataManager(DBHelper dbHelper, SharedPrefsHelper sharedPrefsHelper) {
    this.mDbHelper = dbHelper;
    this.mSharedPrefsHelper = sharedPrefsHelper;
}

Ở đây Dagger sẽ tìm cách khởi tạo DBHelperSharedPrefsHelper.

Tiếp tục, để khởi tạo DBHelper, ta cần @ApplicationContext, @DatabaseInfo String, và @DatabaseInfo Integer:

@Inject
public DBHelper(@ApplicationContext Context context,
                @DatabaseInfo String dbName,
                @DatabaseInfo Integer version) {
    super(context, dbName, null, version);
}

Chính vì vậy, trong lớp ApplicationModule, mình đã thêm 3 phương thức sau:

@Provides
@ApplicationContext
public Context provideApplicationContext() {
    return application;
}

@Provides
@DatabaseInfo
public String provideDBName() {
    return "DaggerExample-DB";
}

@Provides
@DatabaseInfo
public Integer provideDBVersion() {
    return 1;
}

3 phương thức này để cho Dagger sử dụng trong việc khởi tạo DBHelper .

Tiếp đến là khởi tạo SharedPrefsHelper. Ta cần đối tượng SharedPreference:

@Inject
public SharedPrefsHelper(SharedPreferences sharedPreferences) {
    this.mSharedPreferences = sharedPreferences;
}

Vì vậy nên mình lại thêm 1 phương thức @Provides đối tượng SharedPreference:

@Provides
public SharedPreferences provideSharedPreference() {
    return application.getSharedPreferences("DaggerExample-SharedPrefs", Context.MODE_PRIVATE);
}

Vậy là xong. mình đã có đầy đủ các “nguyên liệu” để giúp Dagger tạo ra đối tượng DataManager. Tiếp theo sẽ là việc viết ra ApplicationComponent để làm cầu nối giữa Dependency Provider (Module mà ta vừa viết) với Dependency Consumer (các Activity).

Bước 9, ApplicationComponent

@Singleton
@Component(modules = {ApplicationModule.class})
public interface ApplicationComponent {
    void inject(App app);

    DataManager getDataManager();
}

Rất đơn giản đúng không, đây chỉ là 1 interface được gắn 2 annotation:

@Singleton: chỉ ra rằng component này sẽ tồn tại trong suốt vòng đời ứng dụng, và chỉ có 1 khởi tạo duy nhất.

@Component: chỉ ra module mà nó sẽ đảm nhiệm việc kết nối.

Phương thức inject (App app) chính là phương thức kết nối giữa Dependency Provider và Dependency Consumer (trong trường hợp này ,consumer là lớp App).

Lưu ý: Bạn có thể đặt tên phương thức inject này tuỳ ý, không nhất thiết phải là inject.

Quay lại lớp App mà ta viết ở bước 7. Hãy để ý hàm này:

private void initApplicationComponent() {
    applicationComponent = DaggerApplicationComponent.builder().applicationModule(new ApplicationModule(this)).build();
    applicationComponent.inject(this);
}

Và câu lệnh khai báo:

@Inject
DataManager dataManager;

Hàm initApplicationComponent chính là hàm đảm nhiệm việc khởi tạo ApplicationComponent và inject (kết nối ApplicationModule với App). Lưu ý rằng bạn không thể khởi tạo ApplicationComponent 1 cách trực tiếp qua câu lệnh new, mà phải thông qua builder của Dagger.

DaggerApplicationComponent là lớp mà Dagger 2 tự sinh ra cho chúng ta để khởi tạo ApplicationComponent. Đối với các Component khác cũng tương tự, ta chỉ cần build project, sau đó thêm tiền tố Dagger vào trước tên Component là có thể sử dụng builder để tạo ra đối tương Component.

Sau khi inject xong thì ta có thể thoải mái sử dụng đối tượng DataManager mà không cần phải tự tay khởi tạo nó.

Trên đây là cách thức hoạt động cũng như cách làm việc cơ bản với Dagger 2. Cám ơn bạn đã theo dõi bài viết.

 

 

 

Giới thiệu về Dagger 2, cách sử dụng Dependency Injection in Android (Phần 1)

Chào các bạn, đã lâu rồi mình không có dịp viết bài về phát triển ứng dụng. Hôm nay mình sẽ gửi đến các bạn 1 seri bài viết về Dagger 2, một thư viện Dependency Injection hết sức mạnh mẽ và hữu ích dành cho phát triển Android.

Do thông tin mình muốn truyền tải khá là nhiều, nên mình sẽ chia ra làm phần, để cho các bạn mới dễ đọc và nắm bắt hơn.

Phần 1: Giới thiệu các khái niệm về Dependency Injection, sơ qua về thư viện Dagger 2.

Phần 2: Tất nhiên là ví dụ thực tế rồi.

Bắt đầu nhé.

[toc]

Dependency Injection (DI) là gì

Dependency Injection được xây dựng dựa trên khái niệm Inversion of Control. Nghĩa là các phụ thuộc của 1 class chỉ nên được cung cấp từ bên ngoài. Hiểu đơn giản là không 1 class nào được khởi tạo đối tượng của class khác (class phụ thuộc) bên trong nó, mà nên lấy class phụ thuộc đó từ 1 configuration class. Ví dụ cho dễ hiểu nhé. Ta có 1 class Pet như sau:

public class Pet {

    public Pet() {
        
    }

}

Và 1 class Person, có thuộc tính là 1 đối tượng Pet (nói cách khác, Person phụ thuộc vào Pet)

public class Person {

    Pet pet;

}

Giờ đây ta sẽ có 2 cách viết hàm khởi tạo cho Person. Cách thứ nhất:

public Person() {
    this.pet = new Pet();
}

Cách này không tuân theo nguyên tắc của Inversion of Control, bởi vì class Person đã tự khởi tạo ra phụ thuộc của nó (tức class Pet).

Cách viết hàm khởi tạo cho Person để tuân theo quy tắc Inversion of Control:

public Person(Pet pet) {
    this.pet = pet;
}

Ở cách thứ 2 này ta đã nhúng (Inject) đối tượng Pet từ 1 nguồn khác (nguồn nào thì hiện tại ta chưa cần biết :D). Và lúc này Person không cần biết Pet được khởi tạo thế nào, Person chỉ việc sử dụng Pet khi cần thiết.

Tác dụng của Dependency Injection

Tác dụng lớn nhất của việc sử dụng Depedency Injection là việc có thể tăng tính tái sử dụng của các class và giúp ta có thể test chúng 1 cách hoàn toàn độc lập (không phụ thuộc vào nhau).

Như ở trên, Person không cần biết Pet được khởi tạo thế nào, Person chỉ cần quan tâm đến các logic nghiệp vụ mà nó đảm nhiệm. Còn việc khởi tạo ra đối tượng Pet ra sao, đó là việc của Dagger. Hãy thử tưởng tượng Pet có khoảng 10 cách khởi tạo, hoặc khi đoạn code khởi tạo của Pet bị thay đổi => class Person cũng phải thay đổi theo class Pet (Person phụ thuộc vào Pet) => Giá trị mà Dependency Injection đem lại là rất quan trọng.

Làm sao để áp dụng Dependency Injection?

Để trả lời câu hỏi này, ta hãy tìm hiểu 1 chút về lịch sử 🙂

Ngày xưa, có 1 framework có chức năng phân tích sự phụ thuộc của 1 class (xem class đó phụ thuộc vào các class nào). Sau khi phân tích, framework này khởi tạo các class phụ thuộc và nhúng chúng vào class gốc thông qua Java Reflection. Như vậy các class này có thể được test (kiểm thử) 1 cách độc lập. Framework mà chúng ta nói tới chính là Dagger 1.

Nhưng quá trình vận hành của Dagger 1 có 2 nhược điểm: 1 là Reflection là việc rất chậm chạp, và thứ 2: Dagger 1 tạo ra các đối tượng mà class gốc phụ thuộc (dependency) tại thời điểm Runtime, điều này có thể dẫn đến các lỗi không mong muốn.

Sự ra đời của Dagger 2

Dagger 2 được ra đời để khắc phục các nhược điểm của Dagger 1. Thay vì việc khởi tạo các đối tượng tại thời điểm Runtime, Dagger 2 tạo ra 1 sơ đồ phụ thuộc (dependency graph) thông qua các Annotation. Nôm na là các class cung cấp sự phụ thuộc được sinh ra bằng các đoạn code (được generate bởi Dagger 2) trong quá trình Compile time. Điều này làm giảm khả năng gây ra các lỗi không mong muốn. Và may mắn là các đoạn code mà Dagger 2 generate ra để tạo ra các phụ thuộc cũng rất dễ đọc và dễ hiểu 🙂

Quy trình sử dụng Dagger 2

Lan man quá, ở phần này mình sẽ trình bày qua quy trình sử dụng Dagger 2.

Lớp cần sử dụng (dependency consumer) yêu cầu lớp cung cấp (dependency provider) thông qua đối tượng kết nối (connector)

  • Lớp cung cấp (dependency provider) là 1 class được gắn annotation @Module, có trách nhiệm khởi tạo ra các đối tượng để Inject (gắn) vào các lớp cần sử dụng (dependency consumer). Trong Dependency provider định nghĩa các hàm được gắn annotation @Provides. Các hàm này trả về các đối tượng mà dependency consumer cần sử dụng. Ví dụ như:
@Provides
public Pet providePet(){
    return new Pet();
}

Hàm này sẽ cung cấp đối tượng Pet cho dependency consumer (trong trường hợp này là class Person) sử dụng.

  • Lớp cần sử dụng (dependency consumer): sử dụng annotation @Inject để được nhúng các đối tượng phụ thuộc. Ví dụ:
@Inject
Pet pet;
  • Đối tượng kết nối (connector): là 1 interface được gắn annotation @Component có chức năng kết nối giữa @Module (lớp cung cấp) và các lớp cần sử dụng (dependency consumer). Thực thể của interface này sẽ được Dagger tạo ra và ta không cần (và cũng không thể) khởi tạo trực tiếp.

Kết luận

Như vậy bài này mình đã giới thiệu cái nhìn sơ bộ về dependency injection và quy trình sử dụng thư viện Dagger 2 để phục vụ công tác dependency injection này. Mình hiểu là đối với những người mới thì mấy thứ này khá phức tạp và mình đã diễn giải theo cách dễ hiểu nhất có thể. Bài sau mình sẽ đi vào ví dụ thực tế để cho các bạn dễ hiểu và hình dung hơn. Cảm ơn đã theo dõi.

 

 

Collection trong Java

Trong java có 1 framework gọi là Collections. Framework này được đặt trong package java.utils. Nó cung cấp 1 tập hợp các generic class bao gốm các hàm xử lý dữ liệu liên quan đến list mà Array không thể giúp chúng ta giải quyết.

Hãy thử tìm hiểu 1 số phương thức cần biết khi làm việc với Collections

Xóa 1 phần tử của List trong vòng lặp for.

Việc xóa 1 phần tử trong khi duyệt list là 1 việc làm tương đối thường xuyên trong các bài toán thực tế. Ví dụ như ta có 1 list chứa các đối tượng NewsAds. (Dạng này hay gặp ở các app tin tức, khi ta muốn chèn các quảng cáo vào list danh sách tin). Giờ ta chỉ muốn lọc ra các đối tượng News (tức là xóa bỏ đi các đối tượng Ads) trong list. Ta sẽ làm thế nào. Mình sẽ chỉ ra 1 số cách làm sai, và 1 số cách làm đúng. Hãy theo dõi.

Mình có listNews như sau:

List listNews = new ArrayList<>();

listNews.add(new NewsItem());

listNews.add(new NewsItem());

listNews.add(new NewsItem());

listNews.add(new AdsItem());

listNews.add(new AdsItem());

listNews.add(new AdsItem());

Và mình muốn xóa các AdsItem.

Các phương thức làm sai thường gặp

Cách làm sai 1:

for(int i=0;i<listNews.size();i++){

    if(listNews.get(i) instanceof AdsItem){

    listNews.remove(i);

    }

}

Với cách làm này, ta sẽ thử kiểm nghiệm kết quả:

for(int i=0;i<listNews.size();i++){

    System.out.println(listNews.get(i).getClass());

}

Kết quả:

class NewsItem

class NewsItem

class NewsItem

class AdsItem

Vì sao cách này sai. Trong vòng lặp, đối tượng AdsItem cuối ko bị xóa đi do i chỉ chạy đến 3, do phần tử AdsItem trước bị xóa. Dẫn đến size của list bị giảm xuống 4 => i chỉ chạy đến 3. Đồng nghĩa với việc đối tượng AdsItem cuối cùng (chỉ số ban đầu là 4) – không bị động tới.

Cách làm sai 2:

for(Object o:listNews){

    if(o instanceof AdsItem){

        listNews.remove(o);

    }

}

Cách này sẽ gây ra exception  java.util.ConcurrentModificationException. Nguyên nhân bởi ta đã duyệt và xóa phần tử tại cùng 1 thời điểm.

Cách làm đúng

Sử dụng Iterator:

Iterator iterator = listNews.iterator();

    while(iterator.hasNext()){

        if(iterator.next() instanceof AdsItem){

        iterator.remove();

    }

}

Ta thử kiểm nghiệm kết quả:

class NewsItem

class NewsItem

class NewsItem

Lúc này ta đã sử dụng đến Iterator, đây là interface cung cấp cho ta phương thức remove dùng để xóa phần tử trong list tạo ra nó.

Cách 2: Sử dụng list should-be-removed:

Ý tưởng chính của cách này là sao chép các phần tử cần xóa sang 1 list mới, sau đó sử dụng hàm removeAll đối với list cũ.

List shoudBeRemoved = new ArrayList<>();

for (Object o : listNews) {

    if (o instanceof AdsItem) {

    shoudBeRemoved.add(o);

    }

}

listNews.removeAll(shoudBeRemoved);

Cách 3: Sử dụng removeIf

Cách này cực kỳ đơn giản so với 2 cách trên:

listNews.removeIf(p-> p instanceof AdsItem);

Done!

Sub Collection

Đang viết…

Removing matching items from Lists using Iterator

Đang viết…

Join lists

Đang viết

Generics trong Java

[toc]

Định nghĩa

Generic programming trong Java là việc ta cho phép 1 phương thức hay 1 class xử lý các đối tượng thuộc 1 kiểu dữ liệu tuỳ ý (generic). Kiểu dữ liệu tuỳ ý này có dạng  compile-time type safety. Tức là 1 khi nó đã được khai báo, trình biên dịch sẽ không chấp nhận việc phương thức hay class chứa xử lý các kiểu dữ liệu khác.

Để dễ hiểu hơn mình sẽ lấy 1 ví dụ đơn giản thế này. Ta khai báo 1 ArrayList như sau:

ArrayList<String> listName = new ArrayList<>();

Bạn thấy listName là 1 ArrayList và nó chỉ chấp nhận các giá trị String. Ở đây ArrayListclass chứa và String chính là kiểu dữ liệu “tuỳ ý” mà mình nói ở trên.

Mình lại lấý 1 ví dụ khác:

ArrayList<Integer> listNumber = new ArrayList<>();

Lần này thì ArrayList lại chỉ chấp nhận xử lý kiểu dữ liệu Integer, bởi vì Integer đã được khai báo với ArrayList.

Tạo 1 generic class (lớp chứa)

Giống như mọi class khác, generic class được khai báo với từ khoá class. Điểm khác biệt ở đây là nó còn có thêm cặp <T> để tượng trưng cho kiểu dữ liệu mà nó sẽ xử lý. Ví dụ:

public class Param<T> {
	T value;

	public T getValue() {
		return value;
	}

	public void setValue(T value) {
		this.value = value;
	}
}

Như các bạn thấy <T> tượng trưng cho kiểu dữ liệu sẽ được xử lý bởi Param. Nó có thể là bất cứ kiểu gì: Integer, String, Boolean, Animal, Human, ….. Nhưng 1 khi đã được khai báo, lớp chứa của nó (generic class) sẽ chỉ chấp nhận xử lý kiểu dữ liệu đã được khai báo.

Giờ mình sẽ tạo ra 1 đối tượng Param chuyên xử lý kiểu dữ liệu Integer:

Param<Integer> paramInteger = new Param<>();
paramInteger.setValue(10); // lúc này hàm setValue chỉ chấp nhận giá trị có kiểu Integer
System.out.println(paramInteger.value); //in ra 10

Mình sẽ tạo 1 đối tượng Param khác chuyên xử lý kiểu String:

Param<String> paramString= new Param<>();
paramString.setValue("cungdev.com"); //// lúc này hàm setValue chỉ chấp nhận giá trị có kiểu String
System.out.println(paramString.value); //in ra cungdev.com

Tương tự, bạn có thể khai báo bất cứ kiểu dữ liệu nào để generic class Param xử lý.

Kế thừa generic class

Việc kế thừa generic class thực chất chẳng khác gì kế thừa 1 class bình thường

Ví dụ mình khai báo 1 generic class AbstractParam như sau (lớp này sẽ được kế thừa bởi các lớp khác):

public abstract class AbstractParam<T> {
	protected T value;
	protected abstract void printValue();

}

Không khác gì 1 lớp bình thường ngoài việc khai báo thêm kiểu dữ liệu generic <T> phải không?

Giờ ta sẽ tạo 1 lớp Email kế thừa AbstractParam:

public class Email extends AbstractParam<String>{

	@Override
	protected void printValue() {
		System.out.println("My email is:"+value);
		
	}	

}

Ta đã tạo 1 lớp Email kế thừa AbstracParam<String>, điều này có nghĩa là:

  • Ta đã chỉ định generic type là String
  • Thuộc tính value trong lớp Email sẽ có kiểu là String.

Ta sẽ test 2 điều trên bằng câu lệnh sau:

Email email = new Email();
email.value = "duongtuanvu1111@gmail.com";
email.printValue();//in ra My email is:duongtuanvu1111@gmail.com
email.value = 10; //Khong duoc, vi value bat buoc phai mang kieu String

Giờ mính sẽ khai báo 1 lớp khác là Age:

public class Age extends AbstractParam<Integer>{

	@Override
	protected void printValue() {
		System.out.println("My age is "+value);
		
	}
	
}

Tương tự như class Email, nhưng lần này Age được khai báo là generic class của kiểu Int, có nghĩa là trường value của Age chỉ có thể mang kiểu Int:

Age age = new Age();
age.value = 10;
age.printValue(); //in ra My age is 10
age.value = "Nam"; //khong duoc vi value bat buoc phai mang kieu Int

Mình sẽ lấy thêm 1 ví dụ nữa, ta khai báo class Height như sau:

public class Height<T> extends AbstractParam<T> {

	@Override
	protected void printValue() {
		System.out.println("My height is "+value);
		
	}

}

Ở đây khi định nghĩa Height, ta chưa khai báo rõ kiểu generic (trong 2 trường hợp trước ta đã chỉ ra luôn StringInteger), tức là Height có thể xử lý bất kỳ kiểu dữ liệu gì mà không bị “ép buộc” phải xử lý StringInteger như class EmailAge. Ví dụ:

Height<Float> heightFloat = new Height<>(); //kieu du lieu se duoc xu ly la Float
heightFloat.value = 10f;
heightFloat.printValue(); //in ra My height is 10.0		
Height<Long> heightLong = new Height<>(); //kieu du lieu se duoc xu ly la Long
heightLong.value = 10000L;
heightLong.printValue(); //in ra My height is 10000

Đó là những kiến thức cơ bản về kế thừa generic class, hy vọng mình diễn đạt dễ hiểu.

Khai báo nhiều dữ liệu generic (Multiple type parameters)

Ta không những có thể khai báo kiểu dữ liệu generic cho class xử lý, ta còn có thể khai báo tuỳ thích số lượng các kiểu generic. Ví dụ:

public class MultiGenericParam<T, S> {
	private T firstParam;
	private S secondParam;

	public MultiGenericParam(T firstParam, S secondParam) {
		this.firstParam = firstParam;
		this.secondParam = secondParam;
	}

	public T getFirstParam() {
		return firstParam;
	}

	public void setFirstParam(T firstParam) {
		this.firstParam = firstParam;
	}

	public S getSecondParam() {
		return secondParam;
	}

	public void setSecondParam(S secondParam) {
		this.secondParam = secondParam;
	}
}

Lớp trên sẽ được sử dụng kiểu thế này:

MultiGenericParam<String, String> aParam = new MultiGenericParam<String, String>("value1", "value2");
MultiGenericParam<Integer, Double> dayOfWeekDegrees = new MultiGenericParam<Integer, Double>(1, 2.6);

T , ? super T và ? extends T

Đôi khi ta không nhất thiết phải khai báo generic Type 1 cách chính xác, ? super T? extends T giúp ta điều này. Cụ thể:

  • ? super T tượng trưng cho các lớp là lớp cha của T
  • ? extends T tượng trưng cho các lớp kế thừa T

Để rõ hơn mình sẽ lấy ví dụ như sau. Mình tạo các class:

class Shoe {}
class IPhone {}
interface Fruit {}
class Apple implements Fruit {}
class Banana implements Fruit {} 
class GrannySmith extends Apple {}						

public class FruitHelper {
    public void eatAll(Collection<? extends Fruit> fruits) {} 
    public void addApple(Collection<? super Apple> apples) {}
						
}

Giờ ta sẽ sử dụng các câu lệnh sau để thấy rõ được cách sử dụng của T, ? super T? extends T

FruitHelper fruitHelper = new FruitHelper();
List<Fruit> fruits = new ArrayList<Fruit>();
fruits.add(new Apple()); 
// Hợp lệ, do Apple kế thừa Fruit nên Apple chính là 1 Fruit 
//ArrayList fruits chấp nhận xử lý các đối tượng Fruit nên Apple cũng được chấp nhận
Collection<Banana> bananas = new ArrayList<>();
bananas.add(new Banana());
//Chấp nhận do ArrayList bananas chấp nhận xử lý các đối tượng Banana
Collection<Apple> apples = new ArrayList<>();
fruitHelper.addApple(apples); // Allowed
apples.add(new GrannySmith());
//Chấp nhận do GrannySmith kế thừa Apple
Collection<GrannySmith> grannySmithApples = new ArrayList<>();
fruitHelper.addApple(grannySmithApples);
//Ko chấp nhận do fruitHelper chỉ chấp nhận list các đối tượng Apple
//Tuy GrannySmith kế thừa Apple, nó là 1 Apple nhưng chưa chắc 1 Apple là 1 GrannySmith
fruitHelper.eatAll(grannySmithApples);
//Chấp nhân, do GrannySmith là 1 Fruit
Collection<Object> objects = new ArrayList<>();
fruitHelper.addApple(objects);
//Chấp nhận do Apple kế thừa class Object

 

 

Visibility – truy cập đến thuộc tính trong class

Thuộc tính private

Thuộc tính private chỉ có thể được truy cập trong class chứa nó. Tức là các class khác không thể truy xuất đến các thuộc tính private. Các thuộc tính này thường được truy xuất đến thông qua các phương thức getter và setter.

Ví dụ, ta tạo 1 class như sau:

public class SomeClass {

	private int variable;

	public int getVariable() {
		return variable;
	}

	public void setVariable(int variable) {
		this.variable = variable;
	}

}

Giờ ta sẽ thử truy xuất đến thuộc tính private int variable:

SomeClass sc = new SomeClass();
 // Không được, vì thuộc tính variable là private
 sc.variable = 7; 
System.out.println(sc.variable); 

// được, vì truy cập variable thông qua hàm setter và getter
sc.setVariable(7); 
System.out.println(sc.getVariable());

Thuộc tính public

Thuộc tính public có thể truy xuất thoải mái từ các class khác.

Ví dụ ta khai báo 1 class:

public class TestPublicVariable {
	public int variable; 
}

Ta thử truy xuất đến thuộc tính public int variable:

Test t = new Test(); 
t.variable = 5;
System.out.println(t.number); //in ra 5

Thuộc tính protect

Thuộc tính protect có thể được truy xuất từ trong class chứa nó, trong package chứa class chứa nó và các class kế thừa class chứa nó. Ví dụ:

Ta tạo 1 class:

public class ParentClass {
    protected int value = 10;
}

Trong cùng package chứa ParentClass ta tạo 1 class Test để thử truy xuất đến thuộc tính value:

public class Test {

	public static void main(String[] args) throws IOException {
		
		ParentClass parent = new ParentClass();
		parent.value = 10;
		System.out.println(parent.value);//in ra 10
	}


}

Ta tạo 1 package khác (giả sử new_package), trong package này sẽ tạo 1 class ChildClass như sau:

public class ChildClass {

	public static void main(String[] args) throws IOException {
		
		ParentClass parent = new ParentClass();
		//Không được vì khác package
		parent.value = 10;
		System.out.println(parent.value);//in ra 10
	}


}

Như vậy là 2 class khác package sẽ không truy cập được các thuộc tính protected của nhau.

Giờ ta sẽ cho ChildClass kế thừa ParentClass:

public class ChildClass extends ParentClass{

	public static void main(String[] args) throws IOException {
		
		ParentClass parent = new ParentClass();
		//ok
		parent.value = 10;
		System.out.println(parent.value);//in ra 10
	}


}

Lúc này ChildClass sẽ truy xuất được thuộc tính protected value của ParentClass.

Package Visibility

Nếu không chỉ ra modifier, thuộc tính sẽ được truy xuất bởi các class cùng package class chứa nó.

Ta tạo 1 class:

public class TestVariable {
	public int variable; 
}

Tạo tiếp 1 class cùng packge với class trên:

public class OtherClass{

	public static void main(String[] args) throws IOException {
		
		TestVariable test= new TestVariable ();
		test.variable = 10;
		System.out.println(test.test);
	}


}

Bạn hãy thử tạo 1 class ngoài package chứa TestVariable, bạn sẽ thấy nó không thể truy xuất đến thuộc tính variable của class TestVariable.

Class và object

Class và object

Một chút về định nghĩa

Trong Java, object là các đối tượng có các thuộc tính (properties) và hành động (method).

Ví dụ: 1 con mèo là 1 đối tượng (object) có thuộc tính color = yellow (lông màu vàng) và phương thức eat() – hành động ăn.

Class là thứ đặc trưng cho các đối tượng cùng kiểu. Ví dụ class Cat đặc trưng cho các object mèo (là những đối tượng có các thuộc tính color và phương thức eat.

Khai báo class

Ta sử dụng từ khóa class để khai báo class:

public class Cat {

	String color;
	String name;

	public Cat(String name, String color) {
		this.color = color;
		this.name = name;
	}

	public void eat() {
		System.out.println(name + " is eating");
	}
}

Ở trong class Cat này ta đã khai báo các thuộc tính color và name cho các đối tượng Cat.

Ta cũng khai báo 1 phương thức eat cho các đối tượng Cat.

Đồng thời ta cũng xây dựng 1 phương thức cho phép tạo ra các đối tượng Cat – phương thức này gọi là constructor – hàm khởi tạo (sẽ nói ở phần sau):

public Cat(String name, String color) {
     this.color = color;
     this.name = name;
}

Như các bạn thấy hàm khởi tạo này cho phép tạo ra đối tượng Cat có thuộc tính name và color.

Khởi tạo đối tượng

Ta sẽ tạo ra 1 đối tượng Cat như sau:

Cat cat = new Cat(“Lyly”, “yellow”);

Giờ đây ta đã có thể sử dụng các phương thức trong class Cat:

cat.eat(); //in ra Lily is eating

Nạp chồng phương thức – Overloading methods

Trong 1 class, đôi khi với 1 phương thức ta có thể truyền các đầu vào khác nhau. Java cho phép ta tạo ra nhiều phương thức có cùng tên, nhưng khác đầu vào.

Ví dụ class Cat trên đã có sẵn phương thức eat – ko truyền giá trị đầu vào. Giả sử ta muốn khai báo thêm 1 phương thức eat(String food) truyền thêm đầu vào là food. Ta sẽ làm như sau:

public void eat(String food){
    System.out.println(name + " is eating "+food);
}

Như vậy 1 phương thức eat khác đã được tạo ra và cho phép ta truyền giá trị đầu vào là food – tên thức ăn. Ta sẽ sử dụng phương thức này:

Cat secondCat = new Cat("Miumiu", "Black");
secondCat.eat("Carrot"); //in ra: Miumiu is eating Carrot

Tương tự với hàm khởi tạo, ta cũng có thể tạo ra nhiều hàm khởi tạo với các giá trị truyền vào khác nhau. Ta sẽ thêm 1 hàm khởi tạo mới cho lớp Cat:

public Cat(String name) {
    this.name = name;
}

Hàm này chỉ cho phép truyền vào name mà ko cho truyền thêm color như ban đầu. Ta sẽ thử tạo 1 đối tượng bằng hàm khởi tạo mới:

Cat thirdCat = new Cat("Kiki");
thirdCat.eat("candy"); // in ra Kiki is eating candy

Khi nào thì được nạp chồng hàm

Khi số lượng các tham số truyền vào hàm là khác nhau

Ví dụ:

method();
method(String param1);
method(String param1, String param2);

Khi thứ tự các tham số truyền vào là khác nhau

Ví dụ:

method(int i, float f);
method(float f, int i);
method(int i, float f, int i2);
method(int i2, float f, int i)// Không được, vì kiểu và thứ tự của các tham số trùng với hàm trên, cùng là (int, float, int)

Ghi đè phương thức – override method

Ghi đè phương thức là việc định nghĩa lại 1 phương thức đã có từ trước. Ví dụ thế này, ta có 1 class Shape:

public class Shape {
    public float getArea(){
	return 1f;
    }
}

Shape có phương thức tính diện tích getArea trả về giá trị float = 1. Giờ ta sẽ ghi đè (override) phương thức getArea này.

Đầu tiên ta khai báo 1 lớp Square kế thừa Shape:

public class Square extends Shape {

    float size;
    public Square(float size) {
        super();
	this.size = size;
    }


}

Nói qua cho các bạn nào chưa rõ, việc 1 classA kế thừa 1 classB có nghĩa là classA được thừa hưởng các phương thức public và thuộc tính public của classB. Hay nói cách khác đối tượng classA sẽ có các phương thức và thuộc tính public đã định nghĩa trong classB.

Ở trường hợp này Square kế thừa Shape. Vì vậy 1 đối tượng Square sẽ có thể sử dụng phương thức getArea (tính diện tích) của Shape. Ví dụ:

Square square = new Square(4);
System.out.println("Area = "+square.getArea()); // in ra :Area = 1.0

Nhưng tất nhiên, Square (hình vuông) sẽ có 1 cách tính diện tích (getArea) riêng. Và ta cần ghi đè (viết lại) phương thức getArea() trong class Square như thế này:

@Override
public float getArea() {
    return size * size;
}

Ta đã sử dụng annotation @Override để chỉ ra phương thức getArea đã được ghi đè. 

Khi này ta sẽ có:

Square square = new Square(4);
System.out.println("Area = "+square.getArea()); // in ra :Area = 16.0

Nếu ta tạo 1 class Circle (hình tròn), và kế thừa Shape, ta cũng có thể ghi đè override lại phương thức getArea():

public class Circle extends Shape {
	float r;

	public Circle(float r) {
		super();
		this.r = r;
	}

	@Override
	public float getArea() {
		return (float) Math.PI * r * r;
	}
}

Và ta thử:

Circle circle = new Circle(2);
System.out.println("Area = "+circle.getArea());// in ra Area = 12.566371

Hàm khởi tạo

Hàm khởi tạo là 1 phương thức đặc biệt để tạo ra 1 đối tượng. Cũng giống như các phương thức khác, hàm khởi tạo có thể nhận các giá trị đầu vào. Chỉ có điều hàm khởi tạo không trả về giá trị nào.

Như ví dụ ở trên:

public class Circle extends Shape {
	float r;

	public Circle(float r) {
		super();
		this.r = r;
	}

	@Override
	public float getArea() {
		return (float) Math.PI * r * r;
	}
}

Ở đây ta thấy:

public Circle(float r) {
    super();
    this.r = r;
}

chính là hàm khởi tạo của lớp Circle.

Những điều cần lưu ý về hàm khởi tạo:

  • Hàm khởi tạo có thể mang bất kỳ modifier nào (public, private, protected), nhưng không được khai báo dưới dạng abstract, static, hay synchronized.
  • Hàm khởi tạo ko trả về giá trị.
  • Hàm khởi tạo bắt buộc phải có tên trùng với tên class
  • từ khóa this sử dụng trong hàm khởi tạo dùng để chỉ đối tượng hiện tại (đối tượng mà hàm khởi tạo đó tạo ra)

Thuộc tính static

Java cho phép ta sử dụng 1 biến static làm thuộc tính của class. Vậy thuộc tính static này khác với thuộc tính thông thường mà ta đã biết ở chỗ nào?

Mình tạo 1 class:

class ObjectMemberVsStaticMember {
	static int staticCounter = 0;
	int memberCounter = 0;

	void increment() {
		staticCounter++;
		memberCounter++;
	}
}

Ở đây ta tạo 1 class chứa 1 thuộc tính kiểu intmemberCounter và thuộc tính static int staticCounter

Giờ mình sẽ chỉ ra sự khác nhau giữa 2 thuộc tính này:

final ObjectMemberVsStaticMember o1 = new ObjectMemberVsStaticMember(); 
final ObjectMemberVsStaticMember o2 = new ObjectMemberVsStaticMember(); 
o1.increment(); 
o2.increment(); 
o2.increment(); 
System.out.println("o1 static counter =" + o1.staticCounter); 
System.out.println("o1 member counter =" + o1.memberCounter);

System.out.println("o2 static counter =" + o2.staticCounter); 
System.out.println("o2 member counter =" + o2.memberCounter); 
System.out.println(); 
System.out.println("ObjectMemberVsStaticMember.staticCounter = " + ObjectMemberVsStaticMember.staticCounter);

Ta tạo ra 2 đối tượng o1o2, 2 đối tượng này cùng gọi đến phương thức increment. Tổng cộng là 3 lời gọi đến phương thức increment.Ta sẽ xem log kết quả để thấy sự khác biệt giữa memberCounter  staticCounter. Kết quả là:

o1 static counter =3 
o1 member counter =1 
o2 static counter =3 
o2 member counter =2 
ObjectMemberVsStaticMember.staticCounter = 3

Thuộc tính static là thành phần của class và chỉ tồn tại duy nhất trong class đó. Tức là bạn có thể tạo ra hàng trăm đối tượng, nhưng các thuộc tính static thì chỉ được tạo ra duy nhất 1 lần.

Thuộc tính thường (non static) thì được tạo ra với từng đối tượng của class.

Như ở ví dụ trên: thuộc tính staticCounter được tạo ra duy nhất 1 lần, và mỗi lần gọi hàm increment, giá trị của nó đều tăng lên 1.

Thuộc tính memberCounter thì khác, mỗi đối tượng của lớp ObjectMemberVsStaticMember được sinh ra, thì sẽ có 1 biến memberCounter mới được sinh ra. Như bạn thấy giá trị memberCounter của o1  memberCounter của o2 là khác nhau.

 

Các kiểu dữ liệu nguyên thủy (Primitive Data Types)

Dữ liệu nguyên thủy (Primitive Data Types) là các kiểu dữ liệu được Java cung cấp sẵn cho chúng ta sử dụng. Trong Java 8 có các kiểu dữ liệu cơ bản sau:  byte, short, int, long, char, boolean, float, và double.

Ta sẽ lần lượt điểm qua về các kiểu dữ liệu này.

[toc]

char

Kiểu char dùng để chứa 1 ký tự đơn dạng Unicode 16 bit. Ký tự này được chứa trong cặp dấu nháy đơn. Ví dụ:

char myChar = 'u'; 
char myChar2 = '5'; 
char myChar3 = 65; // myChar3 == 'A'

Lưu ý: char chỉ được dùng để lưu trữ ký tự đơn. Không có chuyện:

char myChar = 'xyz';

Giá trị nhỏ nhất của 1 biến kiểu Char là 0 (2^0-1) và giá trị lớn nhất của biến char là 65535 (2^16-1).

Để biểu diễn ký tự nháy đơn. Ta làm như thế này:

char singleBlock = '\'';

Biểu diễn các ký tự đặc biệt khác dưới dạng char:

char tab = '\t'; 
char backspace = '\b'; 
char newline = '\n'; 
char carriageReturn = '\r'; 
char formfeed = '\f'; 
char singleQuote = '\''; //dấu nháy đơn
char doubleQuote = '\"'; //dấu nháy kép
char unicodeChar = '\uXXXX' // XXXX là mã ký tự trong bảng mã Unicode

Bạn có thể biểu diễn bất cứ ký tự nào trong bảng mã unicode bằng kiểu dữ liệu char:

char heart = '\u2764';
System.out.println(Character.toString(heart)); // in ra "❤"

float

Kiểu dữ liệu Float sử dụng để biểu diễn các số thực 32 bit. Mặc định trong Java, 1 số thực sẽ được hiểu là mang kiểu Double, vì vậy khai báo biến Float ta cần thêm hậu tố f vào sau giá trị biến. Ví dụ:

double doubleExample = 0.5; // không có hậu tố f, => kiểu double
float floatExample = 0.5f; // có hậu tố f, => kiểu float

float myFloat = 92.7f; // 1 biến kiểu float
float positiveFloat = 89.3f; // biến float mang giá trị dương
float negativeFloat = -89.3f; // biến float mang giá trị âm
float integerFloat = 43.0f; // biến float có thể mang giá trị của 1 số nguyên (nhưng nó không phải int) 

1 biến Kiểu dữ liệu Float có thể sử dụng các toán tử cộng, trừ nhân chia và lấy module:

// cộng
float result = 37.2f + -2.6f; // result: 34.6 
// trừ
float result = 45.1f - 10.3f; // result: 34.8 
// nhân
float result = 26.3f * 1.7f; // result: 44.71 GoalKicker.com – Java® Notes for Professionals 76 
// chia
float result = 37.1f / 4.8f; // result: 7.729166 
// phần dư
float result = 37.1f % 4.8f; // result: 3.4999971

Lưu ý:

  • Lưu ý: : Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY, Float.NaN là những giá trị biểu diễn cho dương vô cùng, âm vô cùng, và not a number.
  • Float.NaN biểu diễn cho kết quả của những phép tính không thể xác định giá trị, vd như phép chia của 2 giá trị vô cùng. Ví dụ:
System.out.println(1f / 0f); // dương vô cùng
System.out.println(1f / -0f); // âm vô cùng
System.out.println(Float.POSITIVE_INFINITY / Float.POSITIVE_INFINITY); // NaN (not a number)

int

int biểu diễn các giá trị nguyên 32 bit (các giá trị nằm trong khoảng -(2^32)+1 và 2^32-1. Ví dụ:

int example = -42;
int myInt = 284;
int anotherInt = 73; 
int addedInts = myInt + anotherInt; // 284 + 73 = 357
int subtractedInts = myInt - anotherInt; // 284 - 73 = 211

Nếu ta cần biểu diễn những giá trị ngoài khoảng Int, ta cần sử dụng kiểu dữ liệu long.

Các giá trị lớn nhất và nhỏ nhất của int:

nt high = Integer.MAX_VALUE; // GTLN == 2147483647 
int low = Integer.MIN_VALUE; // GTNN == -2147483648 

Giá trị mặc định của int là 0:

int defaultInt; // defaultInt == 0;

double

Tương tự như float, double biểu diễn kiểu số thực. Nhưng vùng giá trị của double lớn hơn float. Double biểu diễn cho kiểu số thực 64 bit. Mặc định 1 số thực sẽ được Java hiểu là kiểu double. Ta có thể định nghĩa 1 số thực như sau:

double example = -7162.37; //Mặc định số thực được hiểu là double
double myDouble = 974.21d; //Có thể thêm hậu tố d vào sau giá trị
double anotherDouble = 658.7; 
double addedDoubles = myDouble + anotherDouble; // 315.51
double subtractedDoubles = myDouble - anotherDouble; // 1632.91 
double scientificNotationDouble = 1.2e-3; // 0.0012

Giá trị mặc định của double là 0.

public double defaultDouble; // defaultDouble == 0.0

Lưu ý:

  • Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NaN là những giá trị biểu diễn cho dương vô cùng, âm vô cùng, và not a number.
  • Double.NaN biểu diễn cho kết quả của những phép tính không thể xác định giá trị, vd như phép chia của 2 giá trị vô cùng. Ví dụ:
System.out.println(1f / 0d); // Dương vô cùng
System.out.println(1f / -0d); // Âm vô cùng
System.out.println(Float.POSITIVE_INFINITY / Float.POSITIVE_INFINITY); // NaN (not a number)

long

long biểu diễn các giá trị nguyên 64 bit (các giá trị nằm trong khoảng -(2^64)+1 và 2^64-1. Ví dụ:

long example = -42L;
long myLong = 284L;
long anotherLong = 73L;

//Trong java 1 số nguyên mặc định được hiểu là có kiểu int
//1 số nguyên lớn, ví dụ như 549755813888 (2 ^ 39), lớn hơn giá trị max của 
// kiểu int (2^31 - 1), bắt buộc phải thêm hậu tố L
long bigNumber = 549755813888L;
long addedLongs = myLong + anotherLong; // 284 + 73 = 357
long subtractedLongs = myLong - anotherLong; // 284 - 73 = 211

Các giá trị lớn nhất và nhỏ nhất của long:

long high = Long.MAX_VALUE; // high == 9223372036854775807L
long low = Long.MIN_VALUE; // low == -9223372036854775808L

Giá trị mặc định của long là 0:

long defaultLong; // defaultLong == 0L

Chú ý:

Dù ta khai báo kiểu long, nhưng với những biến có giá trị nằm trong khoảng -127 đến 127, biến đó vẫn mang giá trị của 1 đối tượng Integer. Ví dụ:

Long val1 = 127L;
Long val2 = 127L;
System.out.println(val1 == val2); // true
Long val3 = 128L;
Long val4 = 128L;
System.out.println(val3 == val4); // false

Để so sánh chính xác 2 biến long, ta cần làm như sau:

Long val3 = 128L;
Long val4 = 128L;
System.out.println(Objects.equal(val3, val4)); // true

Lúc này kết quả so sánh đã mang giá trị true.

boolean

1 biến kiểu boolean chứa 1 trong 2 giá trị true hoặc false:

boolean foo = true;
System.out.println("foo = " + foo); // foo = true
boolean bar = false;
System.out.println("bar = " + bar); // bar = false
boolean notFoo = !foo;
System.out.println("notFoo = " + notFoo); // notFoo = false
boolean fooAndBar = foo && bar;
System.out.println("fooAndBar = " + fooAndBar); // fooAndBar = false
boolean fooOrBar = foo || bar;
System.out.println("fooOrBar = " + fooOrBar); // fooOrBar = true
boolean fooXorBar = foo ^ bar;
System.out.println("fooXorBar = " + fooXorBar); // fooXorBar = true

Giá trị mặc định của boolean là false.

byte

byte biểu diễn số nguyên 8 bit nằm trong khoảng -128 đến 127.

byte example = -36;
byte myByte = 96;
byte anotherByte = 7;
byte addedBytes = (byte) (myByte + anotherByte); // 103
byte subtractedBytes = (byte) (myBytes - anotherByte); // 89

Giá trị lớn nhất và nhỏ nhất của byte:

byte high = Byte.MAX_VALUE; // high == 127
byte low = Byte.MIN_VALUE; // low == -128

Giá trị mặc định của byte là 0:

byte defaultByte; // defaultByte == 0

short

short biểu diễn giá trị nguyên trong khoảng -32768 đến 32767

short example = -48;
short myShort = 987;
short anotherShort = 17;
short addedShorts = (short) (myShort + anotherShort); // 1,004
short subtractedShorts = (short) (myShort - anotherShort); // 970

Giá trị lớn nhất và nhỏ nhất của short:

short high = Short.MAX_VALUE; // high == 32767
short low = Short.MIN_VALUE; // low == -32768

Giá trị mặc định của short là 0:

short defaultShort; // defaultShort == 0

 

Xây dựng ứng dụng quay số nhanh Android (p1)

Chào các bạn, mình là Vũ.

Hôm nay nhân dịp tí nữa đo đường vì tội vừa đi xe máy vừa gọi điện thoại, mà bản chất cũng chỉ vì sự rắc rối và lóng ngóng khi tìm contact để call. Mình sẽ xây dựng 1 loạt bài tutorial về ứng dụng quay số nhanh trên Android.

Chức năng chủ yếu của ứng dụng này là cho phép tạo ra 1 widget ngoài màn hình home của điện thoại chứa những contact mà ta hay liên lạc. Đồng thời widget này cũng chứa lịch sử cuộc gọi và bàn phím số. Tóm lại nó giúp người dùng đơn giản hóa tối đa thao tác để liên lạc với người khác. Sau đây là video demo của ứng dụng.

[toc]

Chức năng và luồng chạy của ứng dụng

Chức năng chi tiết

Các chức năng của ứng dụng mà mình sẽ làm bao gồm:

  • Thêm, bớt 1 contact vào danh sách quay số nhanh (trên app)
  • Xem danh sách quay số nhanh (trên app), cho phép gọi, nhắn tin (đến 1 hoặc nhiều người).
  • Tạo widget ngoài màn hình home, widget này chứa
    • Danh sách quay số nhanh, cho phép gọi điện thoại khi click vào contact
    • Lịch sử cuộc gọi, cho phép gọi điện thoại khi click vào 1 lịch sử
    • Bàn phím số, cho phép nhập số điện thoại để gọi

Khá đơn giản, đúng không nào.

Luồng chạy của ứng dụng

  • Ban đầu bạn cần vào app và định nghĩa ra danh sách quay số nhanh. Bằng cách chọn lựa các contact từ danh bạ điện thoại. Danh sách quay số nhanh này có thể thêm bớt, tùy chỉnh về sau.
  • Sau khi có danh sách quay số nhanh, bạn có thể kéo widget của ứng dụng ra ngoài màn hình home. Widget này có chứa danh sách bạn đã tạo trên app, lịch sử cuộc gọi, bàn phím, cho phép make a phone call 1 cách dễ dàng nhất.

Luồng chạy cũng không có gì phức tạp, nhỉ?

Khởi tạo dự án

Các thư viện sử dụng

Mình sẽ sử dụng những thư viện sau đây để code:

//BUtter Knife dùng để bind view    
compile "com.jakewharton:butterknife:$rootProject.butterKnifeVersion"
//SDP android, thư viện chứa các dimensions, hỗ trợ làm layout đa màn hình
compile "com.intuit.sdp:sdp-android:$rootProject.ext.sdpAndroidVersion"
//Material dialog, giao diện dialog material cho android đời thấp
compile 'com.afollestad.material-dialogs:core:0.9.4.4'
//Thư viện log
compile "com.orhanobut:logger:$rootProject.loggerVersion"
//ImageView bo góc, mình lười nên dùng lib luôn
compile 'com.makeramen:roundedimageview:2.3.0'
//HIệu ứng ripple khi click vào các view
compile 'com.balysv:material-ripple:1.0.2'
//Gson dùng để parse json 
compile 'com.google.code.gson:gson:2.4'
//Thư viện danh bạ điện thoại
compile 'com.github.tamir7.contacts:contacts:1.1.7'
//Thư viện expandable recyclerview, thư viện này mình import từ 1 module trong project
compile project(':libs:expandablerecyclerview')

Tạo base cho dự án

Permission

Ứng dụng cần quyền truy cập danh bạ, lịch sử cuộc gọi, gọi điện thoại và gửi tin nhắn. Vì vậy ta cần thêm các dòng sau vào file AndroidManifest.xml:

<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.CALL_PHONE"></uses-permission>
<uses-permission android:name="android.permission.READ_CALL_LOG"></uses-permission>
<uses-permission android:name="android.permission.SEND_SMS"></uses-permission>

BaseActivity

Ở ứng dụng này, trên App ta cần xây dựng 2 màn hình:

  • Danh sách quay số nhanh
  • Màn hình Chọn lựa contact từ danh bạ để thêm vào danh sách quay số nhanh.

Mình sẽ sử dụng 2 Activity để xây dựng 2 màn hình này. Để code được ngắn gọn và tường minh, mình sẽ viết 1 lớp BaseActivity.java. Lớp này chứa 1 số phương thức chung có thể sử dụng cho tất cả các Activity trong dự án. Mọi Activity sẽ kế thừa từ lớp BaseActivity này:

public abstract class BaseActivity extends AppCompatActivity {

    protected Gson gson;
    protected DBHelper dbHelper;
 
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(getLayoutId());
        gson = new Gson();
        dbHelper = QuickDialApplication.getInstance().getDbHelper();
        ButterKnife.bind(this);
        createView();
    }
    //Hàm abstract trả về layout của activity
    protected abstract int getLayoutId();
 
    //Hàm set sự kiện cho các view trong activity
    protected abstract void createView();
 
    //Hàm show thông báo toast
    public void showToast(String msg) {
        if (msg != null) {
            Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "null", Toast.LENGTH_SHORT).show();
        }
    }
 
    //Hàm chuyển activity
    public void showActivity(Class t) {
        Intent intent = new Intent(this, t);
        startActivity(intent);
    }
 
    ////Hàm chuyển activity kèm theo bundle
    public void showActivity(Class t, Bundle bundle) {
        Intent intent = new Intent(this, t);
        intent.putExtra(Constant.KEY_EXTRA, bundle);
        startActivity(intent);
    }
}

Màn hình danh sách quay số nhanh QuickDialActivity

Khai báo layout

Sau khi tạo xong base dự án ta sẽ bắt  tay vào màn hình đầu tiên, là màn hình Danh sách quay số nhanh. Mình đặt tên Activity tương ứng là QuickDialActivity. Mình tạo 1 file activity_quick_dial.xml làm layout cho activity này:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            android:contentInsetEnd="0dp"
            android:contentInsetLeft="0dp"
            android:contentInsetRight="0dp"
            android:contentInsetStart="0dp"
            app:contentInsetEnd="0dp"
            app:contentInsetLeft="0dp"
            app:contentInsetRight="0dp"
            app:contentInsetStart="0dp"
            app:popupTheme="@style/AppTheme.PopupOverlay">

            <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent">

                <ImageView
                    android:id="@+id/iv_icon"
                    android:layout_width="@dimen/_27sdp"
                    android:layout_height="wrap_content"
                    android:layout_centerVertical="true"
                    android:layout_marginLeft="@dimen/_10sdp"
                    android:adjustViewBounds="true"
                    android:src="@drawable/ic_icon" />

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_centerVertical="true"
                    android:layout_marginLeft="@dimen/_10sdp"
                    android:layout_toRightOf="@id/iv_icon"
                    android:text="@string/app_name"
                    android:textColor="@android:color/white"
                    android:textSize="16sp"
                    android:textStyle="bold" />

                <ImageView
                    android:id="@+id/bt_add"
                    android:layout_width="@dimen/_26sdp"
                    android:layout_height="wrap_content"
                    android:layout_alignParentRight="true"
                    android:layout_centerVertical="true"
                    android:layout_marginRight="@dimen/_10sdp"
                    android:adjustViewBounds="true"
                    android:padding="@dimen/_5sdp"
                    android:src="@drawable/ic_add_people" />

                <ImageView
                    android:id="@+id/bt_remove"
                    android:layout_width="@dimen/_26sdp"
                    android:layout_height="wrap_content"
                    android:layout_centerVertical="true"
                    android:layout_marginRight="@dimen/_10sdp"
                    android:layout_toLeftOf="@+id/bt_add"
                    android:adjustViewBounds="true"
                    android:padding="@dimen/_5sdp"
                    android:src="@drawable/ic_edit" />

                <TextView
                    android:id="@+id/bt_confirm_remove"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_alignParentRight="true"
                    android:layout_centerVertical="true"
                    android:layout_marginRight="@dimen/_10sdp"
                    android:text="@string/action_remove"
                    android:textColor="@android:color/white"
                    android:textSize="15sp"
                    android:textStyle="bold"
                    android:visibility="gone" />

                <TextView
                    android:id="@+id/bt_send_message"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_centerVertical="true"
                    android:layout_marginRight="@dimen/_10sdp"
                    android:layout_toLeftOf="@+id/bt_confirm_remove"
                    android:text="@string/action_message"
                    android:textColor="@android:color/white"
                    android:textSize="15sp"
                    android:textStyle="bold"
                    android:visibility="gone" />
            </RelativeLayout>


        </android.support.v7.widget.Toolbar>

    </android.support.design.widget.AppBarLayout>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        tools:showIn="@layout/activity_quick_dial">

        <GridView
            android:id="@+id/gv_quick_dial"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:columnWidth="@dimen/_54sdp"
            android:horizontalSpacing="@dimen/_10sdp"
            android:numColumns="auto_fit"
            android:verticalSpacing="@dimen/_10sdp"></GridView>

        <LinearLayout
            android:id="@+id/ll_add"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:gravity="center"
            android:orientation="vertical"
            android:visibility="gone">

            <ImageView
                android:layout_width="@dimen/_40sdp"
                android:layout_height="wrap_content"
                android:adjustViewBounds="true"
                android:src="@drawable/ic_layout_add" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="@dimen/_10sdp"
                android:gravity="center"
                android:text="@string/no_contact"
                android:textSize="16sp" />
        </LinearLayout>

        <android.support.design.widget.FloatingActionButton xmlns:app="http://schemas.android.com/apk/res-auto"
            android:id="@+id/bt_setting"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_alignParentRight="true"
            android:layout_margin="@dimen/_10sdp"
            android:src="@drawable/ic_setting"
            app:backgroundTint="@color/colorPrimaryDark" />
    </RelativeLayout>
</android.support.design.widget.CoordinatorLayout>

Layout khi danh sách quay số nhanh đã có contact trông sẽ thế này:

Về cơ bản, layout này chứa:

  • GridView để hiển thị danh sách quay số nhanh. (gv_quick_dial)
  • Layout thông báo chưa có liên lạc nào trong danh sách, click vào layout này sẽ chuyển sang màn hình chọn liên lạc (ll_add)
  • 1 vài button chức năng khác (ta chưa cần quan tâm)

Vậy 2 chức năng đầu tiên mà ta cần xây dựng là:

  • Lấy được danh sách quay số nhanh
  • HIển thị được danh bạ điện thoại để chọn liên lạc.

Danh sách quay số nhanh

Các liên hệ trong danh sách quay số nhanh cần được lưu trữ trong 1 database để ta có thể đem ra sử dụng. Ở đây mình dùng luôn SQLite. Vậy công việc đầu tiên của ta là xây dựng 1 database bằng SQLite cho phép thêm, xóa 1 contact vào trong database. Mình sẽ tạo 1 package db và tạo file DBHelper.java. Lớp DBHelper này sẽ quản lý việc thêm, xóa dữ liệu về contact vào database. Cụ thể:

  • Lưu dữ liệu từ đối tượng Contact vào bảng QContact (thuộc thư viện danh bạ mà mình đã nêu ra ban đầu, đối tượng này chứa các thông tin của 1 contact trong danh bạ điện thoại)
  • Lấy các dữ liệu đã lưu theo dạng List các đối tượng WidgetContactModel để hiển thị ra ở trên Widget ngoài màn hình home.

Ok, vậy giờ mình sẽ định nghĩa đối tượng WidgetContactModel trước. Mình tạo 1 package models, và file WidgetContactModel.class:

public class WidgetContactModel implements Parcelable {

    int id;
    String displayName;
    String numbers;
    String avatar;

    byte[] avatarBitmap;

    public WidgetContactModel(int id, String displayName, String numbers, String avatar) {
        this.id = id;
        this.displayName = displayName;
        this.numbers = numbers;
        this.avatar = avatar;
    }

    public WidgetContactModel(int id, String displayName, String numbers, String avatar, byte[] avatarBitmap) {
        this.id = id;
        this.displayName = displayName;
        this.numbers = numbers;
        this.avatar = avatar;
        this.avatarBitmap = avatarBitmap;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getDisplayName() {
        return displayName;
    }

    public void setDisplayName(String displayName) {
        this.displayName = displayName;
    }

    public String getNumbers() {
        return numbers;
    }

    public void setNumbers(String numbers) {
        this.numbers = numbers;
    }

    public String getAvatar() {
        return avatar;
    }

    public void setAvatar(String avatar) {
        this.avatar = avatar;
    }

    public byte[] getAvatarBitmap() {
        return avatarBitmap;
    }

    public void setAvatarBitmap(byte[] avatarBitmap) {
        this.avatarBitmap = avatarBitmap;
    }

    public WidgetContactModel(Parcel in) {
        id = in.readInt();
        displayName = in.readString();
        numbers = in.readString();
        avatar = in.readString();
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel parcel, int i) {
        parcel.writeInt(id);
        parcel.writeString(displayName);
        parcel.writeString(numbers);
        parcel.writeString(avatar);
    }

    public static final Creator CREATOR = new Creator() {
        @Override
        public Object createFromParcel(Parcel parcel) {
            return new WidgetContactModel(parcel);
        }

        @Override
        public WidgetContactModel[] newArray(int i) {
            return new WidgetContactModel[i];
        }
    };
}

Xong, giờ mình sẽ quay lại viết nội dung cho DBHelper thực hiện các tác vụ của 1 database:

public class DBHelper extends SQLiteOpenHelper {

    private static final String DB_NAME = "quick-dial-db";
    private static final int DB_VERSION = 3;

    private static final String CREATE_TABLE_CONTACT = "create table QContact(id integer primary key autoincrement not null, display_name text, numbers text, avatar text, avatar_bitmap blob);";


    public DBHelper(Context context) {
        super(context, DB_NAME, null, DB_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_TABLE_CONTACT);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int i, int i1) {
        db.execSQL("drop table if exists QContact");
        onCreate(db);
    }

    public ArrayList<WidgetContactModel> getListContact() {
        String query = "select * from QContact";
        Cursor cursor = getReadableDatabase().rawQuery(query, null);
        ArrayList<WidgetContactModel> listContact = new ArrayList<>();
        if (cursor.getCount() > 0) {
            cursor.moveToFirst();
            do {
                WidgetContactModel contact;
                contact = new WidgetContactModel(cursor.getInt(0), cursor.getString(1), cursor.getString(2), cursor.getString(3), cursor.getBlob(4));
                listContact.add(contact);
            } while (cursor.moveToNext());
        }
        cursor.close();
        return listContact;
    }

    public boolean checkContactSaved(ContactModel contact, int position) {
        Cursor cursor = getReadableDatabase().query("QContact", new String[]{"id", "display_name", "numbers", "avatar"}, "display_name = ? and numbers = ?", new String[]{contact.getTitle(), contact.getItems().get(position).getPhoneNumber()}, null, null, null);
        boolean saved = false;
        if (cursor.getCount() > 0) {
            cursor.moveToFirst();
            do {
                boolean firstCondition = cursor.getString(cursor.getColumnIndex("display_name")).equalsIgnoreCase(contact.getTitle());
                if (!firstCondition) {
                    continue;
                }
                boolean secondCondition = false;
                String numbers = cursor.getString(cursor.getColumnIndex("numbers"));
                secondCondition = numbers.equalsIgnoreCase(contact.getItems().get(position).getPhoneNumber());
                saved = firstCondition && secondCondition;
                if (saved) {
                    break;
                }
            } while (cursor.moveToNext());
            cursor.close();
            return saved;
        } else {
            cursor.close();
            return saved;
        }
    }

    public boolean checkContactSaved(Contact contact, int position) {
        Cursor cursor = getReadableDatabase().query("QContact", new String[]{"id", "display_name", "numbers", "avatar"}, "display_name = ? and numbers = ?", new String[]{contact.getDisplayName(), contact.getPhoneNumbers().get(position).getNumber()}, null, null, null);
        boolean saved = false;
        if (cursor.getCount() > 0) {
            cursor.moveToFirst();
            do {
                boolean firstCondition = cursor.getString(cursor.getColumnIndex("display_name")).equalsIgnoreCase(contact.getDisplayName());
                if (!firstCondition) {
                    continue;
                }
                boolean secondCondition = false;
                String numbers = cursor.getString(cursor.getColumnIndex("numbers"));
                secondCondition = numbers.equalsIgnoreCase(contact.getPhoneNumbers().get(position).getNumber());
                saved = firstCondition && secondCondition;
                if (saved) {
                    break;
                }
            } while (cursor.moveToNext());
            cursor.close();
            return saved;
        } else {
            cursor.close();
            return saved;
        }
    }


    public long saveContact(ContactModel contact, int numberPosition, byte[] avatarBitmap) {
        if (!checkContactSaved(contact, numberPosition)) {
            LogUtils.d("contact save true");
            ContentValues contentValues = new ContentValues();
            contentValues.put("display_name", contact.getTitle());
            StringBuffer numbersBuffer = new StringBuffer();
            numbersBuffer.append(contact.getItems().get(numberPosition).getPhoneNumber());
            contentValues.put("numbers", numbersBuffer.toString());
            contentValues.put("avatar", (contact.getUriAvatar() != null && !contact.getUriAvatar().isEmpty()) ? contact.getUriAvatar() : "");
            contentValues.put("avatar_bitmap", avatarBitmap);
            return getWritableDatabase().insert("QContact", null, contentValues);
        } else {
            LogUtils.d("contact save false");
            return -1;
        }
    }

    public long removeContact(ContactModel contact, int numberPosition) {
        return getWritableDatabase().delete("QContact", "display_name = ? and numbers = ?",
                new String[]{contact.getTitle(), contact.getItems().get(numberPosition).getPhoneNumber()});
    }

    private long removeContact(WidgetContactModel contact) {
        return getWritableDatabase().delete("QContact", "display_name = ? and numbers = ?",
                new String[]{contact.getDisplayName(), contact.getNumbers()});
    }

    public List<Boolean> removeContacts(List<WidgetContactModel> listContact) {
        List<Boolean> listResult = new ArrayList<>();
        for (WidgetContactModel contactModel : listContact) {
            listResult.add(removeContact(contactModel) > 0);
        }
        return listResult;
    }
}

Như vậy là đã xong các phương thức quản lý dữ liệu. Tiếp theo ta sẽ làm đến bước hiển thị chúng ra màn hình.

Hiển thị danh sách quay số nhanh

Ta sẽ xây dựng adapter hiển thị danh sách quay số nhanh. adapter này sẽ được sử dụng sau khi lấy được danh sách quay số nhanh từ cơ sở dữ liệu. Mình tạo 1 package adapter và tạo file ListQuickDialAdapter.java.

Adapter này sẽ có 2 dạng item:

  • Item hiển thị contact
  • Item hiển thị button Add Contact

Vậy mình sẽ phải xây dựng layout cho 2 dạng item này. Với item hiển thị contact, mình tạo 1 file item_quick_dial.xml như sau:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="@dimen/_54sdp"
    android:layout_height="wrap_content">

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/layout_item_widget"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="@dimen/_7sdp">

        <com.makeramen.roundedimageview.RoundedImageView xmlns:app="http://schemas.android.com/apk/res-auto"
            android:id="@+id/iv_avatar"
            android:layout_width="@dimen/_40sdp"
            android:layout_height="@dimen/_40sdp"
            android:layout_gravity="center_horizontal"
            android:scaleType="fitXY"
            app:riv_mutate_background="true"
            app:riv_oval="true" />
        

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="@dimen/_3sdp"
            android:gravity="center"
            android:lines="2"
            android:maxLines="2"
            android:text="BH"
            android:textSize="11sp"
            android:textStyle="bold" />
    </LinearLayout>

    <CheckBox
        android:layout_alignParentRight="true"
        android:id="@+id/cb_remove"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
         />
</RelativeLayout>

Đơn giản là chứa ImageView hiển thị avatar, TextView hiển thị tên và 1 CheckBox để đánh dấu chọn contact (chức năng này mình sẽ nói sau)

Đối với item hiển thị Button add thêm contact, mình tạo 1 file item_add_more.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <LinearLayout
        android:id="@+id/layout_item_widget"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="@dimen/_7sdp">

        <com.makeramen.roundedimageview.RoundedImageView xmlns:app="http://schemas.android.com/apk/res-auto"
            android:id="@+id/iv_avatar"
            android:layout_width="@dimen/_40sdp"
            android:layout_height="@dimen/_40sdp"
            android:layout_gravity="center_horizontal"
            android:scaleType="fitXY"
            android:src="@drawable/ic_add"
            app:riv_mutate_background="true"
            app:riv_oval="true" />
        

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="@dimen/_3sdp"
            android:gravity="center"
            android:lines="2"
            android:maxLines="2"
            android:text="BH"
            android:textSize="11sp"
            android:textStyle="bold"
            android:visibility="invisible" />
    </LinearLayout>

</RelativeLayout>

Đã xong phần layout, giờ ta sẽ viết nội dung cho ListQuickDialAdapter.java.

public class ListQuickDialAdapter extends BaseAdapter {

    Context context;
    List<WidgetContactModel> listContact;
    LayoutInflater layoutInflater;
    boolean selectionMode = false;
    List<WidgetContactModel> listToRemove;

    public ListQuickDialAdapter(Context context, List<WidgetContactModel> listContact) {
        this.context = context;
        this.listContact = listContact;
        layoutInflater = LayoutInflater.from(context);
        listToRemove = new ArrayList<>();
    }

    @Override
    public int getCount() {
        return listContact.size() + 1;
    }

    @Override
    public WidgetContactModel getItem(int i) {
        return listContact.get(i);
    }

    @Override
    public long getItemId(int i) {
        return i;
    }

    @Override
    public int getItemViewType(int position) {
        if (position < listContact.size()) {
            return 0;
        }
        return 1;
    }

    @Override
    public int getViewTypeCount() {
        return 2;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        QuickDialViewHolder quickDialViewHolder = null;
        AddMoreViewHolder addMoreViewHolder = null;
        if (convertView == null) {
            if (getItemViewType(position) == 0) {
                convertView = layoutInflater.inflate(R.layout.item_quick_dial, parent, false);
                quickDialViewHolder = new QuickDialViewHolder(convertView);
                convertView.setTag(quickDialViewHolder);
            } else {
                convertView = layoutInflater.inflate(R.layout.item_add_more, parent, false);
                addMoreViewHolder = new AddMoreViewHolder(convertView);
                convertView.setTag(addMoreViewHolder);
            }
        } else {
            if (getItemViewType(position) == 0) {
                quickDialViewHolder = (QuickDialViewHolder) convertView.getTag();
            } else {
                addMoreViewHolder = (AddMoreViewHolder) convertView.getTag();
            }
        }
        if (getItemViewType(position) == 0) {
            quickDialViewHolder.setData(listContact.get(position), position);
        }
        return convertView;
    }

    class QuickDialViewHolder {

        @Bind(R.id.iv_avatar)
        RoundedImageView ivAvatar;
        @Bind(R.id.tv_name)
        TextView tvName;
        @Bind(R.id.cb_remove)
        CheckBox cbRemove;

        public QuickDialViewHolder(View itemView) {
            ButterKnife.bind(this, itemView);
        }


        public void setData(WidgetContactModel widgetContactModel, int position) {
            if (widgetContactModel.getAvatar() != null && !widgetContactModel.getAvatar().isEmpty()) {
                ivAvatar.setImageURI(Uri.parse(widgetContactModel.getAvatar()));
            } else {
                ivAvatar.setImageResource(R.drawable.default_avatar);
            }
            tvName.setText(widgetContactModel.getDisplayName());
            if (selectionMode) {
                cbRemove.setVisibility(View.VISIBLE);
                if (listToRemove.contains(widgetContactModel)) {
                    cbRemove.setChecked(true);
                } else {
                    cbRemove.setChecked(false);
                }
            } else {
                cbRemove.setVisibility(View.GONE);
                cbRemove.setChecked(false);
            }
            cbRemove.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    if (listToRemove.contains(widgetContactModel)) {
                        cbRemove.setChecked(false);
                        listToRemove.remove(widgetContactModel);
                    } else {
                        cbRemove.setChecked(true);
                        listToRemove.add(widgetContactModel);
                    }
                }
            });

        }
    }

    public void resetMode(boolean selectionMode) {
        this.selectionMode = selectionMode;
        if (selectionMode) {
            listToRemove.clear();
        }
    }


    class AddMoreViewHolder {

        public AddMoreViewHolder(View itemView) {
            ButterKnife.bind(this, itemView);
        }

    }

    public List<WidgetContactModel> getListContact() {
        return listContact;
    }

    public void setListContact(List<WidgetContactModel> listContact) {
        this.listContact = listContact;
    }

    public List<WidgetContactModel> getListToRemove() {
        return listToRemove;
    }

    public void setListToRemove(List<WidgetContactModel> listToRemove) {
        this.listToRemove = listToRemove;
    }

    public boolean isSelectionMode() {
        return selectionMode;
    }

    public void setSelectionMode(boolean selectionMode) {
        this.selectionMode = selectionMode;
    }
}

Lớp này sẽ lấy list dữ liệu từ

List<WidgetContactModel> listContact

để hiển thị lên App.

Ở đây, mình có định nghĩa thêm 1 list nữa:

List<WidgetContactModel> listToRemove

List này là để sau này mình làm chức năng remove 1 contact từ danh sách quay số nhanh, mình sẽ dùng nó để lưu trữ các contact cần remove.

Ok, quay lại QuickDialActivity, ta viết hàm lấy dữ liệu từ database và đổ lên gridView:

    @Override
    protected void onResume() {
        super.onResume();
        if (getQuickDialTask != null) {
            getQuickDialTask.cancel(true);
            getQuickDialTask = null;
        }
        getQuickDialTask = new AsyncTask<Void, Void, List<WidgetContactModel>>() {
            @Override
            protected List<WidgetContactModel> doInBackground(Void... voids) {
                return dbHelper.getListContact();
            }

            @Override
            protected void onPreExecute() {
                super.onPreExecute();
                showLoadingDialog(R.string.app_name, R.string.action_search);
            }

            @Override
            protected void onPostExecute(List<WidgetContactModel> widgetContactModels) {
                super.onPostExecute(widgetContactModels);
                for (WidgetContactModel widgetContactModel : widgetContactModels) {
                    LogUtils.d("[" + getClass().getSimpleName() + "]" + widgetContactModel.getDisplayName() + " with " + widgetContactModel.getNumbers());
                }
                if (adapter == null) {
                    adapter = new ListQuickDialAdapter(QuickDialActivity.this, widgetContactModels);
                    gvQuickDial.setAdapter(adapter);
                } else {
                    adapter.setListContact(widgetContactModels);
                    adapter.notifyDataSetChanged();
                }
                if (!widgetContactModels.isEmpty()) {
                    gvQuickDial.setVisibility(View.VISIBLE);
                    llAdd.setVisibility(View.GONE);
                } else {
                    gvQuickDial.setVisibility(View.GONE);
                    llAdd.setVisibility(View.VISIBLE);
                }
                hideLoadingDialog();
            }
        }.execute();
    }

Tuy nhiên, từ đầu tới giờ ta mới xây dựng module hiển thị danh sách quay số nhanh mà chưa làm module thêm contact vào danh sách quay số nhanh. Chính vì vậy cho tới hiện tại thì database của chúng ta vẫn là database rỗng. Vậy mình sẽ đi tiếp sang chức năng add 1 contact từ danh bạ vào danh sách quay số nhanh.

List contact từ danh bạ mình sẽ hiển thị trong lớp MainActivity. Để cho đầy đủ thì ở QuickDialActivity, mình sẽ set sự kiện click cho ll_add để chuyển qua lớp MainActivity. Đây sẽ là nơi chúng ta implement các phương thức thêm, xóa contact vào database.

    @OnClick(R.id.bt_add)
    public void onClickAdd() {
        showActivity(MainActivity.class);
    }

Hết bài 1

Như vậy bài này mình đã trình bày xong về:

  • Xây dựng database để lưu trữ contact
  • Xây dựng màn hình hiển thị danh sách quay số nhanh

Bài tiếp theo mình sẽ trình bày về cách lấy danh sách liên lạc từ danh bạ và đổ vào cơ sở dữ liệu. Đồng thời hiển thị lên màn hình danh sách quay số nhanh.

Phần widget ngoài màn hình home khá phức tạp, mình sẽ dành ra 2 bài cuối để trình bày. Các bạn theo dõi nhé!