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é!

React Native bài 2: Text, Image và Button

Chào các bạn, mình là Vũ. Hôm nay mình sẽ tiếp tục seri hướng dẫn react native với bài hướng dẫn về các Component cơ bản Text, Image, Button.
Để cho bài viết được dễ hiểu hơn (và dễ viết hơn, đối với mình), mình sẽ làm 1 ứng dụng (cực kỳ đơn giản) để mọi người mới dễ dàng nắm bắt. Ứng dụng cuối cùng của ta trông sẽ như thế này:

Giao diện ứng dụng

Ứng dụng rất đơn giản, chỉ có 1 dòng Text giới thiệu tên mình, có 1 Image hiển thị mặt của mình, và 1 Button. Khi nhấn vào button sẽ hiện lên 1 thông báo. Quá simple phải không? Mình nghĩ là không cần phức tạp làm gì.
Yeah, giờ ta sẽ bắt tay vào công việc nhé.

[toc]

Khởi tạo project

Ta sẽ tạo project và đặt tên cho nó là TextImageButton. Trong command line, gõ câu lệnh:

react-native init TextImageButton

Đợi chút cho project được tạo xong. Ta sẽ mở thư mục dự án trong IDE (ở đây mình dùng SublimeText). Chạy dự án:

react-native run-android

Sau đó ta thiết lập tùy chọn Hot reloading cho ứng dụng.

Giờ bạn hãy để ý trong file index.js (file này sẽ được load đầu tiên khi ứng dụng khởi chạy):

import App from './App';

AppRegistry.registerComponent('TextImageButton', () => App);

Ở đây ta sẽ sử dụng component App ở trong file App.js là component khởi tạo của ứng dụng.

Lưu ý: để sử dụng 1 class không nằm trong file đang làm việc, ta phải import nó. Import 1 class sử dụng câu lệnh

import tên_class from đường_dẫn_file

Lưu ý: tên đường dẫn file không bắt buộc phải có phần mở rộng.

Ok, giờ ta sẽ vọc tiếp vào component App, đây sẽ chính là nơi ta tạo ra giao diện cho ứng dụng này.

StyleSheet

Như bài trước mình đã nói, Component là thứ hiển thị ra giao diện của người dùng. Mỗi Component đều phải có 1 hàm render() để trả ra các View. Component App này cũng vậy. Nó cũng có 1 hàm render(), và hiện tại nó đang trả về 1 View chứa 3 đối tượng Text bên trong. Trước tiên mình sẽ xóa 3 dòng Text đó đi, và tùy chỉnh 1 chút giao diện cho View, sử dụng StyleSheet.

Hàm render của mình hiện tại sẽ thế này:

render() {
    return (
      <View style={styles.containerStyle}>
        
      </View>
    );
  }

Đối với Android và iOS, ta phải sử dụng XML để style cho các thành phần giao diện, thì trong React Native, chúng ta tùy chỉnh giao diện bằng JavaScript. Mọi Component đều có 1 thuộc tính style. Bạn có thể thấy đối tượng View có thuộc tính

style={styles.containerStyle}

Hãy nhìn xuống cuối file App.js. Có 1 hằng số styles được định nghĩa sẵn như vậy:

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'white',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#00FF00',
    marginBottom: 5,
  },
});

Đây chính là nơi tùy chỉnh các giao diện của các đối tượng được render ra bởi Component. Bạn có thể thấy nó gần giống với cấu trúc của CSS. Và đối tượng View ở hàm render đang sử dụng đối tượng con containerStyle thuộc const styles để làm style hiển thị. Giờ mình muốn thay đổi màu nền thành màu hồng cho nam tính, mình sẽ sửa lại đối tượng container như sau:

containerStyle: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#ffc1c1',
  }

và xóa đi 2 đối tượng instruction và welcome không dùng đến. Styles của chúng ta giờ sẽ thế này:

const styles = StyleSheet.create({
  containerStyle: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#ffc1c1',
  }
});

Giờ save lại ta đã thấy sự thay đổi:

Ok, như vậy là đã đổi đc background của màn hình.
Tóm lại, ta chỉ cần nhớ, để tạo ra style cho các Component ta sử dụng hàm:
StyleSheet.create() rồi định nghĩa các style bên trong. Và tất nhiên không quên import class StyleSheet qua câu lệnh import. Ở đây mình sẽ import 1 lượt tất cả các Component sử dụng trong ứng dụng như sau:

import {
  Platform,
  StyleSheet,
  Text,
  View, 
  Image,
  Alert,
  TouchableOpacity
} from 'react-native';

Chúng ta sẽ sử dụng Styles xuyên suốt quá trình phát triển ứng dụng. Vì vậy mình sẽ nói kỹ hơn ở các phần tiếp tới. Giờ hãy tạo 1 dòng Text trên màn hình nào.

Text

Ở trên ta đã khai báo ViewComponent tổng, nó chứa tất cả các Component con bên trong. Để hiển thị 1 dòng Text, ta sẽ khai báo thêm 1 Component Text nằm trong View như sau:

<View style={styles.containerStyle}>
      <Text style={styles.textStyle}>Xin chào tất cả mọi người, mình là Vũ</Text>
</View>

Việc khai báo đối tượng Text quá đơn giản đúng không. Ở đây component Text có style là đối tượng con textStyle thuộc biến styles. Ta khai báo thêm đối tượng textStyle để tùy chỉnh giao diện cho Text nào:

const styles = StyleSheet.create({
  containerStyle: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#FFC1C1',
  },
  textStyle:{
    color:'#990000',
    fontSize:25,
    textAlign:'center',
    justifyContent:'center',
    margin:40
  }
});

Như vậy là đã khai báo textStyle có màu chữ (color), cỡ chữ (fontSize), thuộc tính căn chỉnh (textAlign), khoảng cách với các đối tượng khác (margin), ta thử save lại và kiểm nghiệm kết quả:

Yeah, trông khá đẹp. Đúng không? Giờ Mình sẽ thêm 1 hình ảnh trên màn hình ứng dụng sử dụng component Image.

Image

Ta sẽ tạo 1 Image và load ảnh từ 1 url trên internet vào image đó. Ở trong View, ta thêm component Image như sau:

<View style={styles.containerStyle}>
      <Text style={styles.textStyle}>Xin chào tất cả mọi người, mình là Vũ</Text>
      <Image style={styles.imageStyle}
             source={{uri: 'https://cungdev.com/wp-content/uploads/2018/02/avatar-300x300.jpg'}}>
      </Image>
</View>

Như các bạn thấy ngoài thuộc tính style như đã trình bày, Image còn có thêm 1 thuộc tính source. Đây chính là thuộc tính chỉ ra nguồn (địa chỉ) hình ảnh sẽ được load ra. ở đây mình truyền vào 1 uri, có giá trị là địa chỉ ảnh trên internet.

Và cuối cùng, ta tạo ra imageStyle bằng cách thêm vào trong biến styles:

imageStyle:{
    width: 200, height: 200,
    marginBottom:20,
    borderTopLeftRadius:10,
    borderTopRightRadius:10,
    borderBottomLeftRadius:10,
    borderBottomRightRadius:10
  }

Như các bạn thấy Image này sẽ có kích thước 200×200, bo 4 góc 10 pixel. Rất đơn giản đúng không. Save lại và ta thấy:

Okie, giờ sẽ đến phần tạo 1 nút bấm nhé.

Button

Trong React Native, ta có thể sử dụng component TouchableOpacity để hiển thị dạng nút bấm (button). Thêm vào đó ta sẽ sử dụng thuộc tính onPress để định nghĩa các hành động khi người dùng bấm vào TouchableOpacity đó.
Đầu tiên,mình sẽ tạo 1 thư mục để chứa ảnh cho button. Trong thư mục gốc project, mình sẽ tạo 1 thư mục img để chứa ảnh, và ném 1 file ảnh có tên button.png vào. Sau đó mình khai báo thêm Component TouchableOpacity như sau:

<TouchableOpacity onPress={() => { Alert.alert('cungdev.com', 'Xin chào các bạn');}}>  
      <Image style={styles.imageButtonStyle} source={require('./img/button.png')}></Image>
</TouchableOpacity>

Thứ nhất, các bạn thấy là ta đã định nghĩa sự kiện khi bấm vào TouchableOpacity thông qua thuộc tính onPress. Ở đây ta sẽ cho hiển thị 1 dialog lên.
Thứ 2, TouchableOpacity chỉ là Component bắt sự kiện onPress, còn để hiển thị giao diện Button theo ý muốn ta cần định nghĩa các Component bên trong TouchableOpacity, như ở bên trên mình sử dụng 1 Image. Ta định nghĩa thêm style cho Image:

imageButtonStyle:{
    width:70, height:70
  }

Thứ 3, các bạn thấy ở đây source của Image không còn là 1 uri trên internet, mà là 1 file ảnh trong project. Mình sử dụng thuộc tính source:

source={require('./img/button.png')}

Chính là require tới file ảnh mà mình mới tạo trong thư mục img ban đầu. Save lại và ta thấy:

và khi bấm vào, ta thấy:

Ok, vậy là đã bắt sự kiện thành công cho button. Đơn giản đúng không.

Tổng kết

Bài này mình đã trình bày xong về các Component cơ bản như View, Text, Image, TouchableOpacity. Mình cũng trình bày sơ qua về cách tùy chỉnh giao diện sử dụng StyleSheet. Trong bài sau mình sẽ nói kỹ hơn về vấn đề style các Component trong ứng dụng. Hãy theo dõi nhé.

Kotlin và Swift, kỷ nguyên mới trong phát triển ứng dụng di động

Cách đây vài tháng, khi Google thông báo rằng họ sẽ sử dụng Kotlin làm ngôn ngữ chính thức trong việc phát triển ứng dụng Android. Mình (và mình đoán là nhiều mobile dev khác) đã cảm thấy rất vui mừng và phấn khích. Mình đã ngay lập tức vào trang chủ của Kotlin để tìm hiểu về những tính năng, cú pháp mới của nó. Rồi sau đó đem Swift ra so sánh với Kotlin. Và mình đã có 1 cảm giác cực kỳ lạ: dường như 1 kỷ nguyên mới trong phát triển ứng dụng di động đã bắt đầu.

Thời kỳ mới bắt đầu

Kotlin cũng như Swift mang đến nhiều sự mới mẻ và thú vị trong cú pháp. Chúng làm cho công việc hằng ngày của chúng ta trở nên nhàn hạ và đơn giản hơn rất nhiều. Không chỉ có thế, chúng mang đến những hỗ trợ đáng kể với những mô hình lập trình mới, và cải tiến những mô hình cũ, đặc biệt là lập trình hàm (functional programming).

Functional programming

Thực tế thì lập trình hàm không phải là khái niệm mới trong phát triển phần mềm. Tuy nhiên với Kotlin và Swift, nó đã được hỗ trợ 1 cách tối đa, từ đó việc phát triển ứng dụng iOS hay Android ngày càng trở nên đơn giản.

Nhớ ngày trước, khi mình mới tập toẹ học lập trình Android, lúc đó, 1 bài toán tính tổng dùng vòng for sẽ được viết kiểu thế này:

String[] mixedArray = new String[] { "4", "5", "a", "-2", "Str" };

int results = 0;

for (String element : mixedArray) {

    results += Integer.parseInt(element);

}

Còn tại thời đim hiện tại? Mọi chuyện đơn gin hơn rt nhiu khi ta có th s dụng 1 câu lệnh duy nht, theo cách tiếp cận ca tư duy lập trình hàm:

Kotlin:

val mixedArray = arrayOf("4", "5", "a", "-2", "Str")

val results = mixedArray

    .filter { obj -> obj.toIntOrNull() != null }

    .map { x -> x.toInt() }

    .reduce { acc, x -> acc + x }

 

Swift:

let mixedArray = ["4", "5", "a", "-2", "Str"]

let results = mixedArray

    .filter({ (obj) -> Bool in return Int(obj) != nil })

    .map { (obj) -> Int in return Int(obj)! }

    .reduce(0, +)

Lambda expression

Năm 2014, biu thức lambda chính thức được đưa vào JDK 8. Nhưng không may, lambda lại không th được s dụng trong lập trình Android, bi Android SDK ch h trợ JDK version 7. Đó cũng chính là lý do th viện kiểu như retrolambda được sinh ra trên đời.

Còn thời đim hiện tại, c 2 ngôn ngữ lập trình Swift và Kotlin đu đã h trợ tính năng này. swift thì là closure còn với Kotlin thì là lambda express.

Swift closure:

{ _ in

    print("Closure is called!")

}

Kotlin lambda express:

{

    println("lambda is called!")

}

Inline initialization

Tiếp tục, với Swift, ta có thể khởi tạo dictionary chỉ với 1 dòng:

let dictionary = ["key1": "value1", "key2": "value2"]

Còn với Java, ta chỉ có thể khởi tạo giá trị của đối tượng Map thông qua 1 static block:

private static final Map<String, String> map;
static
{
    map = new HashMap<String, String>();
    map.put("key1", "value1");
    map.put("key2", "value2");
}

Thực tế, Java cũng đã hỗ trợ hàm MapOf, nhưng phải tới JDK 9 hàm này mới được bổ sung. Còn với Kotlin, mọi chuyện đơn giản hơn rất nhiều:

val map = mapOf<String, String>("key1" to "value1", "key2" to "value2")

Có 1 điều nữa mình muốn đề cập đó là toán tử Range (khoảng). Tính năng thú vị này làm cho công việc code của ta nhàn hạ hơn rất nhiều. Thay vì sử dụng vòng lặp for như thế này:

for (int i = 0; i < N; i++) {
// Do something
}

Thì với Kotlin:

for (i in 0..N-1) {
// Do something
}

hoặc Swift:

for i in 0..<N {
// Do Something
}

Rất ngắn gọn đúng không.

Tuples

Một thành phần nữa của Swift (và Kotlin) cũng rất hay đó là tuples. Nó cho phép ta tạo ra những tập hợp đơn giản, thay vì việc phải định nghĩa thêm class.

Tổng kết

Tổng kết lại, với những tính năng mới mình đã kể trên, và còn rất nhiều tính năng mình chưa nói đến, mình tin là 1 kỷ nguyên mới trong phát triển mobile đã bắt đầu. Developer giờ đây đã có thể dành nhiều thời gian hơn cho việc xử lý logic nghiệp vụ cũng như luồng chạy của ứng dụng. Thay vì tốn thời gian vào việc viết hàng trăm dòng code, chỉ để xử lý những logic đơn giản.

Nếu trước đây bạn phải tốn thời gian cài thêm các thư viện như kiểu PromiseKit, ReactiveCocoa, RXJava để việc code của bạn dễ thở hơn, thì nay, bạn đã được “trang bị” đầy đủ nhất ngay từ khi bắt đầu. Và mình tin điều đó cũng sẽ thúc đẩy những người mới, làm cho họ cảm thấy lập trình ko phải là thứ gì đó khó khăn, vất vả lắm. Mình tin 1 thời kỳ tươi sáng đã thực sự bắt đầu.

React Native bài 1: Cài đặt React Native và viết ứng dụng đầu tiên

React Native là 1 framework phát triển ứng dụng di động do Facebook phát hành. Chỉ với 1 bộ source code viết bằng javascript, bạn có thể build ra ứng dụng cho cả 2 nền tảng iOS và Android rất dễ dàng. Không giống các ứng dụng đa nền tảng khác như web hay hybrid, React Native vẫn sử dụng cốt lõi là các module native của iOS và Android. Từ đó hiệu năng của ứng dụng viết bằng React Native là hoàn toàn vượt trội.

Cộng đồng React Native cũng đang ngày 1 phát triển và nhu cầu tuyển dụng cũng ngày 1 tăng lên. Tương lai của React Native rất là sáng sủa bởi nó được phát triển và hậu thuẫn bởi ông lớn Facebook. Vì vậy nếu bạn có kiến thức về React native thì sẽ là 1 lợi thế rất lớn. Hãy cũng mình tìm hiểu về framework thú vị này trong loại bài tutorial hướng dẫn React Native.

[toc]

Cài đặt React Native

Cài đặt NodeJS

Việc đầu tiên ta phải làm là cài đặt Node để có được trình quản lý package npm. Trong quá trình phát triển ứng dụng React Native ta sẽ phải sử dụng rất nhiều đến giao diện cosole. Bởi vậy nên cài đặt Node là điều bắt buộc.

Các bạn vào đây để download Node JS. Lưu ý: Hiện tại React Native mới chỉ làm việc trơn tru với phiên bản npm 4. Đối với npm version 5 trở lên, hoạt động vẫn chưa được ổn định. Bởi vậy mình khuyến cáo nên tải bản NodeJS chứa npm phiên bản 4:

Chọn NodeJS với npm version 4

Cài đặt React Native

Sau khi cài xong NodeJS, ta tiếp tục cài đặt React Native. Đầu tiên hãy cài đặt CLI của React Native (Command Line Interface). Đây là trình hỗ trợ dòng lệnh console của React Native. Bạn mở console lên và cài đặt CLI sử dụng câu lệnh:

npm install -g react-native-cli

Chờ 1 lúc cho tiến trình chạy xong. Tiếp theo ta cài đặt React Native:

npm install -g create-react-native-app

Như vậy là xong, ta đã hoàn thành xong việc cài đặt React Native.

Cài đặt môi trường phát triển Android

Bạn cần biết rằng, bản chất của React Native là sử dụng javascript chọc xuống dưới nền tảng native (Android, iOS) để sử dụng các nền tảng đó. Javascript chỉ là lớp bề mặt, phần nổi của ứng dụng, nó chỉ có vai trò gọi và sử dụng các thành phần native. Vì vậy để phát triển ứng dụng, bạn vẫn cần phải cài đặt môi trường phát triển. Đối với Android bạn cần:

  • Cài đặt JDK.
  • Cài đặt Android Studio.
  • Thêm biến môi trường JAVA và ANDROID_HOME: là địa chỉ thư mục jdk và android sdk của bạn.
  • Thêm biến môi trường adb: là địa chỉ file adb.exe chứa trong Android SDK.

Cài đặt môi trường phát triển iOS

Tương tự như Android, nếu muốn React Native build ra 1 ứng dụng iOS, bạn cần:

  • Cài đặt MacOS (hoặc mua Macbook), miễn sao bạn có MacOS.
  • Cài đặt XCode.

Như vậy là mình đã hướng dẫn xong phần cài đặt môi trường để phát triển ứng dụng bằng React Native. Tiếp theo, hãy viết ứng dụng đầu tiên.

Sử dụng IDE nào để phát triển React Native

Để code React Native, ta có thể dùng rất nhiều IDE. Tùy sở thích của từng người sẽ chọn ra cho mình 1 IDE để làm việc. Bởi vì cốt lõi của việc phát triển React Native vẫn là native, nên khi bạn đã cấu hình xong môi trường phát triển (Android, iOS) thì việc bạn dùng trình soạn thảo code nào cũng đều OK. Thậm chí bạn có thể dùng Notepad hãy TextEditor để code cũng không vấn đề gì.

Tuy nhiên, mình xin đưa ra 1 vài IDE hữu ích hơn để bạn tham khảo:

Tham khảo cách cấu hình để code React Native cũng như sử dụng từng loại IDE trên ở đây:

https://www.icicletech.com/blog/top-10-editors-for-react-native

Cá nhân mình thì mình dùng Sublime Text cho nhẹ và phù hợp với sở thích.

Ứng dụng React Native đầu tiên

Các bạn tạo 1 project React Native bằng câu lệnh:

react-native init reactTutorialApp

Tiếp theo di chuyển đến thư mục project đã tạo:

cd reactTutorialApp

Như vậy là bạn đang đứng trong thư mục project và sẵn sàng cho việc build ứng dụng. Bạn hãy chạy sẵn 1 máy ảo. Mình sử dụng máy ảo Android mặc định được cung cấp bởi AVD. Sau khi khởi động máy ảo, hãy thử build project bằng câu lệnh:

react-native run-android

Và chờ đợi quá trình build và xem kết quả:

Ok, vậy là ta đã build xong ứng dụng React Native đầu tiên. Khá đơn giản đúng không.

Khái niệm đầu tiên, Component

Sau khi đã chạy thành công project, ta thấy ứng dụng mặc định này đơn giản chỉ hiển thị vài dòng Text lên màn hình. Giờ mình sẽ thay đổi nội dung của chúng xem sao.

Hãy mở IDE của bạn và trỏ thư mục làm việc tới thư mục project. Trong Sublime Text thì nó sẽ thế này:

Tiếp theo hãy chọn file App.js. Mặc định, đây sẽ là file được cấu hình để chạy đầu tiên khi khởi động app. Hãy bỏ qua mọi thứ râu ria và chú ý vào phần khai báo class App:

export default class App extends Component<Props>

Nội dung của class này sẽ quyết định cái gì sẽ được hiển thị lên màn hình ứng dụng.

Ta thấy class App kế thừa Component. Khi bạn code React native, bạn sẽ làm việc với Component rất rất nhiều. Bởi lẽ tất cả những thứ được hiển thị lên trên app đều được kế thừa từ Component. Hay nói cách khác, Component là class đại diện cho các thành phần giao diện. Nó cũng giống như View ở bên Android hay iOS vậy.

Tiếp theo, hãy để ý trong class App có 1 hàm render(). Hàm này sẽ định nghĩa các thành phần hiển thị lên giao diện ứng dụng. Trong mọi class kế thừa từ Component, ta đều phải khai báo hàm này. Các bạn có thể thấy thân hàm render() có nội dung khá giống với 1 cấu trúc HTML. render() trả về 1 đối tượng View chứa 3 đối tượng Text bên trong. ViewText đều được khai báo thông qua các thẻ. ViewText đều kế thừa từ Component.

Ta thấy trong thân của các thẻ Text đều được định nghĩa nội dung hiển thị. Ta sẽ thay đổi nội dung của chúng, ví dụ như sau:

  render() {
    let imageSource = {uri:"http://photos.wikimapia.org/p/00/03/16/67/72_full.jpg"};
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>
          Seri Hướng dẫn React Native
        </Text>
        <Text style={styles.instructions}>
          Xin chào mọi người
        </Text>
        <Text style={styles.instructions}>
          Tôi là Dương Vũ
        </Text>
        
      </View>
    );
  }

Sau đó build lại ứng dụng bằng câu lệnh:

react-native run-android

Kết quả:

Tóm lại, các bạn chỉ cần nhớ rằng, mọi giao diện hiển thị trên app đều kế thừa Component.

Component là 1 lớp chứa hàm render() để khai báo các thành phần giao diện. Trong hàm render này, các giao diện (hay còn gọi là Component) được khai báo qua các thẻ có cấu trúc giống HTML.

Chế độ Hot Reloading

Như các bạn thấy ở phần trên, mỗi khi ta có chỉnh sửa gì đó trong code, ta lại phải build lại ứng dụng qua dòng lệnh. Như vậy thì rất là bất tiện. Nhận thấy điều đó, React Native đã hỗ trợ cho chúng ta chức năng Hot Reloading. Tức là mỗi khi bạn thay đổi và save code, thì lập tức ứng dụng sẽ được load lại một cách tự động.

Enable Hot Reloading trên Android

Để bật tính năng này trên android, bạn build app lên máy rồi gõ dòng lệnh trong console:

adb shell input keyevent 82

Lập tức, 1 developer menu hiện lên, bạn hãy chọn Enable Hot Reloading và Enable Live Reloading.

Enable Hot Reloading trên iOS

Đối với iOS, bạn cũng build app rồi ấn Cmd + D để hiển thị menu developer. Rồi chọn Enable Live Reload:

Sau khi đã bật tính năng này thì mỗi khi các bạn save code, ứng dụng sẽ được tự động load lại. Rất thuận tiện đúng không.

Tổng kết

Vậy là bài này mình đã hướng dẫn các bạn cách cài đặt React Native, build ứng dụng đầu tiên và chỉnh sửa Component Text của ứng dụng. Bài tiếp theo mình sẽ giới thiệu về các khái niệm cơ bản khác của React Native. Đồng thời cũng nói qua về 1 số các Component mà chúng ta sẽ hay gặp trong quá trình phát triển. Hãy theo dõi nhé.