Category Archives: Android-Lập trình Android

Android – Menu

Menu có chức năng hiển thị một danh sách các nút lệnh có liên quan với nhau. Trong các hệ điều hành như Windows, Linux, MacOS… thì menu thường xuất hiện trên thanh menu ở đầu cửa sổ.

Trong Android có 3 loại menu: options (menu tùy chọn), context (menu ngữ cảnh) và menu popup. Chúng ta có thể định nghĩa menu trong file XML và dùng lớp MenuInflater để lấy dữ liệu từ file XML về, hoặc có thể code tay trong Java.

Menu tùy chọn – Options

Menu tùy chọn được hiển thị khi chúng ta bấm nút menu trên thiết bị, thường thì nút này có dạng 3 hoặc 4 dấu gạch ngang.

NKoyD

Ví dụ:

Trong ví dụ dưới đây, chúng ta sẽ tạo menu tùy chọn có 2 item, click vào các item thì dùng Toast để hiển thị thông báo tương ứng.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="vertical" 
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent">
    
    <TextView 
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:text="Options Menu" />
</LinearLayout>

File layout chính không có gì đặc biệt cả, chỉ có một TextView với dòng chữ mô tả đơn giản.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">MainActivity</string>    
    <string name="menu1">Settings</string>
    <string name="menu2">Tools</string>
</resources>

Trong file strings.xml chúng ta định nghĩa 2 biến String dùng làm tiêu đề cho từng menu item.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/settings" 
        android:title="@string/menu1" />
    <item android:id="@+id/tools" 
        android:title="@string/menu2" />
</menu>

Để tạo danh sách các item của menu thì chúng ta tạo trong một file xml riêng và đặt trong thư mục res/menu. Nếu chưa có thư mục này thì chúng ta tự tạo bằng tay. Bên trong file này chúng ta sử dụng thẻ menu để khai báo menu và thẻ item để khai báo các item trong menu đó.

package com.phocode;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.Toast;

public class MainActivity extends Activity
{
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) 
    {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.options_menu, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) 
    {
        switch (item.getItemId()) 
        {
            case R.id.settings:
                Toast.makeText(MainActivity.this, "Settings", 
                    Toast.LENGTH_SHORT).show();
                return true;

            case R.id.tools:
                Toast.makeText(MainActivity.this, "Tools", 
                    Toast.LENGTH_SHORT).show();
                return true;

            default:
                return super.onOptionsItemSelected(item);
        }
    }
}

Để có thể sử dụng menu tùy chọn thì chúng ta phải override lại 2 phương thức của lớp ActivityonCreateOptionsMenu() và onOptionsItemSelected().

@Override
public boolean onCreateOptionsMenu(Menu menu) 
{
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.options_menu, menu);
    return true;
}

Phương thức onCreateOptionsMenu() sẽ thực hiện các công việc khởi tạo menu cho đối tượng Activity, ở đây chúng ta dùng phương thức inflate() của lớp android.view.MenuInflater để lấy dữ liệu của menu từ file options_menu.xml về sử dụng. Phương thức này nhận vào một đối tượng android.view.Menu.

@Override
public boolean onOptionsItemSelected(MenuItem item) 
{
...
}

Phương thức onOptionsItemSelected() sẽ xử lý sự kiện click menu. Phương thức này nhận vào một đối tượng android.view.MenuItem.

case R.id.settings:
    Toast.makeText(MainActivity.this, "Settings", 
        Toast.LENGTH_SHORT).show();
    return true;

Ở đây chúng ta chỉ đơn giản là dùng lớp android.widget.Toast để hiển thị thông báo item nào đã được click.

Screenshot_2016-05-23-11-03-44

Menu ngữ cảnh – Context

Menu ngữ cảnh hiển thị nổi lên trên màn hình nhưng được gắn với một đối tượng View nào đó chứ không liên quan gì tới Activity, menu ngữ cảnh sẽ hiện ra khi chúng ta click và giữ tay trên View đó trong một khoảng thời gian.

Ví dụ:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="vertical" 
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent">
    <ListView 
        android:id="@+id/lvId" 
        android:layout_width="fill_parent" 
        android:layout_height="fill_parent" />     
</LinearLayout>

Ở đây chúng ta sẽ gắn menu ngữ cảnh lên ListView.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/delId" 
        android:title="Delete"/>
    <item android:id="@+id/upId" 
        android:title="Lowercase"/>
    <item android:id="@+id/lowId" 
        android:title="Uppercase"/>
</menu>

Cách định nghĩa menu ngữ cảnh cũng không khác gì so với menu tùy chọn.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">MainActivity</string>
    <string-array name="languages">
        <item>Python</item>
        <item>Java</item>
        <item>Ruby</item>
        <item>C++</item>
    </string-array>
</resources>

Trong file strings.xml chúng ta định nghĩa danh sách các item dùng cho ListView.

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android" 
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent" 
    android:padding="10dp" 
    android:textSize="20sp">
</TextView>

File row.xml sẽ định nghĩa cách các item trong ListView được hiển thị như thế nào.

package com.phocode;

import android.app.Activity;
import android.os.Bundle;
import android.widget.ArrayAdapter; 
import android.widget.ListView; 
import android.view.View;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.Toast;

import java.util.Arrays;
import java.util.ArrayList;

public class MainActivity extends Activity
{
    private ListView lv;
    private ArrayAdapter<String> la;
 
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
 
        lv = (ListView)findViewById(R.id.lvId);
        String[] languages = getResources().getStringArray(R.array.languages);
        ArrayList<String> lst = new ArrayList<String>();
        lst.addAll(Arrays.asList(languages));
 
        la = new ArrayAdapter<String>(this, R.layout.row, lst);
        lv.setAdapter(la);
        registerForContextMenu(lv);
    }
 
    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) 
    {       
        super.onCreateContextMenu(menu, v, menuInfo);
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.context_menu, menu);
    }
 
    @Override
    public boolean onContextItemSelected(MenuItem item) 
    {
        AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
 
        int pos = info.position;
        String i = la.getItem(pos);

        switch (item.getItemId()) 
        {
        case R.id.delId:
            la.remove(i);
            return true;

        case R.id.upId: 
            String upln = i.toUpperCase();
            la.remove(i);
            la.insert(upln, pos); 
            return true;

        case R.id.lowId:
            String lpln = i.toLowerCase();
            la.remove(i);
            la.insert(lpln, pos); 
            return true;

        default:
            return super.onContextItemSelected(item);
        }
    }
}

Để có thể sử dụng menu ngữ cảnh thì chúng ta phải override 2 phương thức là onCreateContextMenu() và onContextItemSelected().

String[] languages = getResources().getStringArray(R.array.languages);
ArrayList<String> lst = new ArrayList<String>();
lst.addAll(Arrays.asList(languages));

Chúng ta lưu các item của ListView trong một đối tượng ArrayList.

registerForContextMenu(lv);

Muốn dùng menu ngữ cảnh cho View nào thì chúng ta gọi phương thức registerForContextMenu() rồi truyền vào đối tượng View đó.

@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) 
{
    super.onCreateContextMenu(menu, v, menuInfo);
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.context_menu, menu);
}

Phương thức onCreateContextMenu() cho menu ngữ cảnh cũng tương tự như phương thức onCreateOptionsMenu() cho menu tùy chọn vậy, ở đây chúng ta dùng lớp MenuInflater để lấy dữ liệu của menu từ file context_menu.xml.

@Override
public boolean onContextItemSelected(MenuItem item) 
{
...
}

Phương thức onContextItemSelected() sẽ làm nhiệm vụ xử lý sự kiện click trên từng item.

AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();

int pos = info.position;
String i = la.getItem(pos);

Lớp AdapterContextMenuInfo có thể lấy một vài thông tin từ lớp MenuItem, trong đoạn code trên chúng ta lấy ra số thứ tự của item trong danh sách và đoạn text của item đó.

case R.id.delId:
    la.remove(i);
    return true;

Trong câu lệnh switch...case, chúng ta kiểm tra xem người dùng đã click vào menu item nào, nếu click vào nút Delete thì chúng ta xóa item của ListView đi.

case R.id.upId:               
    String upln = i.toUpperCase();
    la.remove(i);
    la.insert(upln, pos); 
    return true;

Nếu click vào nút Uppercase thì chúng ta chuyển đoạn text của item trong ListView thành viết hoa, nút Lowercase là viết thường.

Screenshot_2016-05-23-14-25-12

Menu Popup

Menu Popup được hiển thị ngay tại vị trí của View.

Ví dụ:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="vertical" 
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent">
    <Button 
        android:id="@+id/btnId" 
        android:layout_height="wrap_content" 
        android:layout_width="wrap_content" 
        android:layout_marginTop="10dip" 
        android:text="@string/btn_label" 
        android:onClick="onClick" />
    <TextView 
        android:id="@+id/tvId" 
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:layout_marginTop="10dip"/>
</LinearLayout>

Trong file main.xml chúng ta thiết kế một Button và một TextView. Button sẽ hiển thị menu Popup khi được click vào, phương thức xử lý sự kiện click của ButtononClick(). TextView chỉ làm nhiệm vụ hiển thị item nào của menu đã được click thôi.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">MainActivity</string>
    <string name="btn_label">Show menu</string>
    <string name="item1">Item 1</string>
    <string name="item2">Item 2</string>
</resources>

Trong file strings.xml chúng ta định nghĩa một vài biến để làm ID cho Button và làm chuỗi hiển thị menu item.

Menu Popup cũng được thiết kế từ các thẻ menu và item không khác gì với menu ngữ cảnh và menu tùy chọn, ở đây chúng ta thiết kế menu trong file popup_menu.xml, file này được đặt trong thư mục res/menu.

package com.phocode;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.MenuItem;
import android.widget.TextView;
import android.widget.PopupMenu;
import android.widget.PopupMenu.OnMenuItemClickListener;

public class MainActivity extends Activity
    implements OnMenuItemClickListener
{
    private TextView tv;
 
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
 
        tv = (TextView) findViewById(R.id.tvId);
    }
 
    public void onClick(View v)
    { 
        PopupMenu pm = new PopupMenu(this, v);
        pm.getMenuInflater().inflate(R.menu.popup_menu, pm.getMenu());
        pm.setOnMenuItemClickListener(this);
        pm.show();
    }
 
    @Override
    public boolean onMenuItemClick(MenuItem item)
    {
        tv.setText(item.toString() + " selected");
        return true;
    }
}

Chúng ta dùng lớp android.widget.PopupMenu để thao tác với menu.

public void onClick(View v) 
{   
    PopupMenu pm = new PopupMenu(this, v);
    pm.getMenuInflater().inflate(R.menu.popup_menu, pm.getMenu());
    pm.setOnMenuItemClickListener(this);
    pm.show();
}

Trong phương thức onClick(), chúng ta sẽ hiển thị popup menu bằng lớp MenuInflater giống như menu ngữ cảnh và menu tùy chọn. Ngoài ra ở đây chúng ta phải gắn listener cho đối tượng PopupMenu này thông qua phương thức setOnMenuItemClickListener(), khác với menu ngữ cảnh và menu tùy chọn là 2 loại menu này đã có sẵn trong ActivityView nên chúng ta không cần gọi trực tiếp ra như đối tượng menu popup. Sau khi tạo PopupMenu, chúng ta phải gọi phương thức show() nếu muốn hiện menu này ra.

@Override
public boolean onMenuItemClick(MenuItem item) 
{           
    tv.setText(item.toString() + " selected");
    return true;  
}

Bên trong phương thức onMenuItemClick(), chúng ta thiết lập giá trị của TextView là giá trị của menu item đã được click.

Screenshot_2016-05-23-15-53-05

Android – Picker

Trong phần này chúng ta sẽ tìm hiểu về Picker. Picker là lớp View cho phép chúng ta chọn một giá trị nào đó trong một tập các giá trị cho trước, có 2 loại giá trị là số và giờ/ngày tháng.

NumberPicker

NumberPicker cho phép chúng ta chọn một giá trị số trong một tập giá trị cho trước.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="vertical" android:layout_width="fill_parent" 
    android:layout_height="fill_parent">
    
    <NumberPicker android:id="@+id/npId" 
        android:layout_marginTop="5dp" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" />

    <TextView android:id="@+id/tvId" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:layout_marginTop="5dp" 
        android:layout_marginLeft="5dp" 
        android:text="0" android:textSize="30sp" />              
</LinearLayout>

Trong file layout chúng ta khai báo thẻ NumberPicker và thẻ TextView, TextView sẽ hiển thị giá trị đang được chọn trong NumberPicker. 

package com.phocode;

import android.app.Activity;
import android.os.Bundle;
import android.widget.NumberPicker;
import android.widget.TextView;
import android.widget.NumberPicker.OnValueChangeListener;

public class MainActivity extends Activity
{
    private TextView tv;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        tv = (TextView) findViewById(R.id.tvId);

        NumberPicker np = (NumberPicker) findViewById(R.id.npId);

        np.setOnValueChangedListener(new OnValueChangeListener()
        {
            public void onValueChange(NumberPicker picker, int oldVal, 
                int newVal)
            {
                tv.setText(String.valueOf(newVal)); 
            }        
        });

        np.setMaxValue(100);
        np.setMinValue(0);
    }
}

NumberPicker hiển thị giá trị và 2 nút cộng trừ để tăng giảm giá trị.

np.setOnValueChangedListener(new OnValueChangeListener()
{
    public void onValueChange(NumberPicker picker, int oldVal, 
        int newVal)
    {
        tv.setText(String.valueOf(newVal)); 
    }        
});

Khi chúng ta click vào 2 nút cộng trừ thì NumberPicker sẽ giải phóng sự kiện, chúng ta bắt sự kiện này bằng lớp OnValueChangeListener. Ở đoạn code trên chúng ta gắn một đối tượng listener vào picker và sử dụng phương thức onValueChange(), phương thức này sẽ được gọi khi chúng ta click vào 2 nút cộng trừ, tham số của phương thức này là đối tượng picker đã giải phóng sự kiện, giá trị mới sau khi click nút cộng/trừ và giá trị cũ trước khi click nút cộng/trừ.

np.setMaxValue(100);
np.setMinValue(0);

Hai dòng code trên thiết lập khoảng giá trị cho NumberPicker là từ 0 đến 100.

Screenshot_2016-05-22-15-18-08

TimePicker

TimePicker thì hiển thị giá trị giờ, TimePicker có 2 chế độ hiển thị là hiển thị theo dạng 24h hoặc dùng kí hiệu AM/PM. Ngoài ra còn có lớp DatePicker là chọn ngày tháng.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
android:orientation="vertical" 
android:layout_width="fill_parent" 
android:layout_height="fill_parent" >
    
    <TimePicker android:id="@+id/tpId" 
        android:layout_marginTop="5dp" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" />

    <TextView android:id="@+id/tvId" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:layout_marginTop="5dp" 
        android:layout_marginLeft="5dp" 
        android:textSize="30sp" />  
        
</LinearLayout>

Trong file main.xml chúng ta thiết kế một TimePicker và một TextView để hiển thị giá trị của TimePicker đó.

package com.phocode;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TimePicker;
import android.widget.TextView;
import android.widget.TimePicker.OnTimeChangedListener;

public class MainActivity extends Activity
{
    private TextView tv;
    
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
 
        tv = (TextView) findViewById(R.id.tvId);
 
        TimePicker tp = (TimePicker) findViewById(R.id.tpId);
 
        int h = tp.getCurrentHour();
        int m = tp.getCurrentMinute();
 
        StringBuilder tm = new StringBuilder();
        tm.append(h + " h " + m + " m");
        tv.setText(tm);
 
        tp.setOnTimeChangedListener(new OnTimeChangedListener()
        {
            public void onTimeChanged(TimePicker view, int hour, int minute)
            {
                StringBuilder tm = new StringBuilder();
                tm.append(hour + " h " + minute + " m "); 
                tv.setText(tm);
            }
        });      
    }
}

Lớp TimePicker lắng nghe sự kiện bằng lớp OnTimeChangedListener với phương thức onTimeChanged().

tp.setOnTimeChangedListener(new OnTimeChangedListener()
{
    public void onTimeChanged(TimePicker view, int hour, 
        int minute)
    {
        StringBuilder tm = new StringBuilder();
        tm.append(hour + " h " + minute + " m "); 
        tv.setText(tm);
    }        
});

Bên trong phương thức onTimeChanged() chúng ta tạo một chuỗi từ giá trị giờ và phút được gửi tới, sau đó gán làm text cho TextView.

TimePicker tp = (TimePicker) findViewById(R.id.tpId);
 
int h = tp.getCurrentHour();
int m = tp.getCurrentMinute();

StringBuilder tm = new StringBuilder();
tm.append(h + " h " + m + " m");
tv.setText(tm);

Ngoài ra trước đó chúng ta cũng dùng chính lớp TimePicker này để lấy giờ hiện tại của hệ điều hành và gán vào TextView trước.

Screenshot_2016-05-22-15-38-19

Android – Một số View cơ bản – Phần 2

Trong phần này chúng ta tiếp tục tìm hiểu một số lớp View cơ bản là ProgressBarListView.

ProgressBar

ProgressBar hiển thị một thanh ngang hoặc một hình tròn biểu diễn tiến trình hoạt động của một hành động nào đó.

Ví dụ 1:

Chúng ta sẽ thiết kế một ProgressBar  và một TextView hiển thị tiến trình hoạt động theo %.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="vertical" 
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent">
    <ProgressBar 
       android:id="@+id/pbId" 
       android:layout_width="fill_parent" 
       android:layout_height="wrap_content" 
       style="?android:attr/progressBarStyleHorizontal" 
       android:layout_margin="10dp" />       
    <TextView 
        android:id="@+id/tvId" 
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:layout_margin="10dp" />
</LinearLayout>

Trong file main.xml chúng ta thiết kế một thẻ ProgressBar và một TextView. Thuộc tính style quy định kiểu “vẽ” của ProgressBar lên màn hình, ở đây progressBarStyleHorizontal nghĩa là vẽ kiểu thanh ngang. Mặc định thì ProgressBar sẽ vẽ theo kiểu hình tròn.

package com.phocode;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.util.Log;

public class MainActivity extends Activity
{
    private ProgressBar pb;
    private TextView tv;
    private int prg = 0;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        pb = (ProgressBar) findViewById(R.id.pbId);
        tv = (TextView) findViewById(R.id.tvId);
 
        new Thread(myThread).start();
    }

    private Runnable myThread = new Runnable()
    { 
        @Override
        public void run() 
        {
            while (prg < 100)
            {
                try
                {
                    hnd.sendMessage(hnd.obtainMessage());
                    Thread.sleep(100);
                }
                catch(InterruptedException e) 
                {  
                    Log.e("ERROR", "Thread was Interrupted");
                }
            }

            runOnUiThread(new Runnable() { 
                public void run() {
                    tv.setText("Finished");
                }
            });          
        }
    
        Handler hnd = new Handler()
        {    
            @Override
            public void handleMessage(Message msg) 
            {
                prg++;
                pb.setProgress(prg);

                String perc = String.valueOf(prg).toString();
                tv.setText(perc+"% completed");
            }
        };
    };
}

Chúng ta sẽ điều khiển tiến trình hoạt động của ProgressBar trong một luồng khác.

new Thread(myThread).start();

Ở đây chúng ta sử dụng một luồng khác để thực hiện công việc tính toán, còn luồng chính sẽ cập nhật ProgressBar, lý do sử dụng 2 luồng khác nhau là để chúng có thể chạy song song với nhau.

@Override
public void run() 
{
    while (prg < 100)
    {
        try
        {
            hnd.sendMessage(hnd.obtainMessage());
            Thread.sleep(100);
        }
        catch(InterruptedException e) 
        {  
            Log.e("ERROR", "Thread was Interrupted");
        }
    }

    runOnUiThread(new Runnable() { 
        public void run() {
            tv.setText("Finished");
        }
    });          
}

Công việc tính toán ở đây chỉ đơn giản là tăng giá trị của một biến từ 0 đến 100, mỗi lần tăng thì nghỉ 100 mili giây.

Cơ chế của hệ điều hành Android có hơi khác các hệ điều hành khác là những biến nào được khai báo trong một Activity chỉ có thể truy xuất được trong Activity đó, biến myThread tuy thuộc lớp Activity chính nhưng thực chất nó chạy một luồng của riêng nó, không đụng chạm gì tới Activity chính cả, do đó chúng ta không thể thao tác trực tiếp với các biến prg hay biến TextView tv bên trong phương thức run() được. Để có thể thao tác với các biến của một đối tượng Activity từ một luồng khác, chúng ta phải gọi chúng bên trong phương thức runOnUiThread() hoặc bên trong một đối tượng Handler.

runOnUiThread(new Runnable() { 
    public void run() {
        tv.setText("Finished");
    }
});   

Biến thuộc luồng nào thì chỉ có thể truy xuất được từ luồng đó, ở đây chúng ta chỉnh sửa biến tv nên chúng ta truy xuất nó bên trong phương thức runOnUiThread().

Handler hnd = new Handler()
{    
    @Override
    public void handleMessage(Message msg) 
    {
        prg++;
        pb.setProgress(prg);

        String perc = String.valueOf(prg).toString();
        tv.setText(perc+"% completed");
    }
};

Như đã nói, ngoài cách dùng phương thức runOnUiThread(), chúng ta cũng có thể truy xuất các biến đó thông qua một đối tượng Handler. Ở đây chúng ta cập nhật biến prg và thiết lập giá trị của biến đó cho ProgressBar.

Screenshot_2016-05-21-16-00-23

Ví dụ 2:

Trong ví dụ này, chúng ta sẽ hiển thị ProgressBar ở dạng hình tròn.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="vertical" 
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent" >
    <ProgressBar 
        android:id="@+id/pbId" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content"/>    
    <TextView 
        android:id="@+id/tvId" 
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:text="Please wait..." />        
</LinearLayout>

Ở đây chúng ta không thiết lập biến style cho ProgressBar nữa và như thế ProgressBar sẽ hiển thị hình tròn thay vì thanh ngang. Ngoài ra TextView cũng không hiển thị số % tiền trình hoạt động nữa nên chúng ta thiết lập “cứng” thuộc tính text luôn.

package com.phocode;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.view.View;
import android.util.Log;

public class MainActivity extends Activity
{
    private ProgressBar pb;
    private TextView tv;
    private int prg = 0;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        pb = (ProgressBar) findViewById(R.id.pbId);
        tv = (TextView) findViewById(R.id.tvId);
 
        new Thread(myThread).start();
    }

    private Runnable myThread = new Runnable()
    { 
        @Override
        public void run() 
        {
            while (prg < 100)
            {
                try
                {
                    hnd.sendMessage(hnd.obtainMessage());
                    Thread.sleep(100);
                }
                catch(InterruptedException e) 
                {  
                    Log.e("ERROR", "Thread was Interrupted");
                }
            }

            runOnUiThread(new Runnable() { 
                public void run() {
                    tv.setText("Finished");
                    pb.setVisibility(View.GONE); 
                }
            });          
        }
    
        Handler hnd = new Handler()
        {    
            @Override
            public void handleMessage(Message msg) 
            {
                prg++;
                pb.setProgress(prg);
            }
        };
    };
}

Đoạn code trong file MainActivity.java cũng tương tự như ví dụ trước.

runOnUiThread(new Runnable() { 
    public void run() {
        tv.setText("Finished");
        pb.setVisibility(View.GONE); 
    }
}); 

Chỉ khác là ở đây chúng ta không cập nhật text cho TextView theo % mà chỉ canh khi nào tiến trình hoàn tất thì mới cập nhật thôi. Ngoài ra sau khi hoàn tất công việc, chúng ta cho ẩn ProgressBar đi bằng phương thưc setVisibility().

Screenshot_2016-05-21-15-06-18

ListView

ListView hiển thị dữ liệu dưới dạng một danh sách các item, chúng ta có thể dùng tay cuộn danh sách lên xuống nếu danh sách quá dài. ListView sử dụng dữ liệu được cung cấp bởi lớp Adapter.

Ví dụ 1:

<?xml version="1.0" encoding="utf-8"?>  
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="vertical"
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent">  
     
    <ListView 
        android:id="@+id/lvId" 
        android:layout_width="fill_parent" 
        android:layout_height="fill_parent" />  
    
</LinearLayout> 

Trong file main.xml chúng ta khai báo một thẻ ListView.

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android" 
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent" 
    android:padding="10dp" android:textSize="20sp">
</TextView>

File row.xml sẽ định nghĩa cách các hàng trong ListView được hiển thị như thé nào, ví dụ như các hàng có độ lớn bao nhiêu, kích cỡ chữ bao nhiêu. Ở đây sp là đơn vị dùng cho kích thước font trong Android.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">MainActivity</string>
    <string-array name="languages">
        <item>Python</item>
        <item>Java</item>
        <item>Ruby</item>
        <item>C++</item>
    </string-array>        
</resources>

Trong file strings.xml chúng ta định nghĩa các item sẽ được dùng cho ListView trong biến string-array.

package com.phocode;

import android.app.Activity;
import android.os.Bundle;
import android.widget.ArrayAdapter;  
import android.widget.ListView;  

public class MainActivity extends Activity
{
    private ListView lv;  
    private ArrayAdapter<String> la; 

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        setupUI();
    }

    public void setupUI()
    { 
        lv = (ListView) findViewById(R.id.lvId);  
        String[] languages = getResources().getStringArray(R.array.languages); 
        lv.setAdapter(new ArrayAdapter<String>(this, R.layout.row, languages));
    }
}

Trong file MainActivity.java chúng ta kết nối tất cả những thứ trên lại với nhau.

String[] languages = getResources().getStringArray(R.array.languages);

Dòng code trên sẽ lấy dữ liệu của các item trong file strings.xml đưa vào mảng languages.

lv.setAdapter(new ArrayAdapter<String>(this, R.layout.row, languages));

Lớp ArrayAdapter có nhiệm vụ kết nối file row.xml, dữ liệu trong mảng languages vào đối tượng ListView.

Screenshot_2016-05-21-15-23-13

Ví dụ 2:

Trong ví dụ này chúng ta sử dụng lớp ListActivity thay cho lớp Activity thường. Lớp ListActivity là lớp Activity nhưng có sẵn một đối tượng ListView. Bởi vì ListView rất thường được dùng để hiển thị riêng trong một màn hình Activity, do đó Android cho ra đời lớp ListActivity để đơn giản hóa việc thiết kế cho các coder. Cũng chính vì vậy ở đây chúng ta cũng sẽ không dùng đến file main.xml để thiết kế.

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android" 
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent" 
    android:padding="10dp" android:textSize="20sp">
</TextView>

File row.xml định nghĩa cách ListView hiển thị, giống như ví dụ trước.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">MainActivity</string>
</resources>

File strings.xml chỉ lưu tiêu đề của ứng dụng chứ không còn lưu danh sách các item nữa.

package com.phocode;

import android.app.ListActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;  
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ListView;  
import android.widget.TextView;

public class MainActivity extends ListActivity 
     implements OnItemClickListener, OnItemSelectedListener
{       
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
      
        ArrayAdapter<String> la = new ArrayAdapter<String>(this, R.layout.row);  
        la.add("Python");
        la.add("Java");
        la.add("Ruby");
        la.add("C++");

        setListAdapter(la);

        ListView lv = getListView();
        lv.setOnItemClickListener(this);
        lv.setOnItemSelectedListener(this);
    }

    public void onItemClick(AdapterView<?> parent, View view,
        int position, long id) 
    {        
        String language = ((TextView) view).getText().toString();
        setTitle(language);
    }

    public void onItemSelected(AdapterView<?> parent, View view,
        int position, long id) 
    {        
        String language = ((TextView) view).getText().toString();
        setTitle(language);
    }

    public void onNothingSelected(AdapterView<?> parent) 
    {        
       
    }
}

Thay vào đó chúng ta sẽ thiết kế các item cho ListView trong Java. Ngoài ra chúng ta bắt sự kiện click vào item trên ListView nữa.

public class MainActivity extends ListActivity 
     implements OnItemClickListener, OnItemSelectedListener

Lớp MainActivity giờ đây sẽ kế thừa lớp ListActivity và implement 2 giao diện là OnItemClickListenerOnItemSelectedListener, tổng cộng có 3 phương thức trừu tượng cần được code.

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

Ở đây phương thức onCreate() sẽ không có dòng setContentView() vì mặc định lớp ListActivity đã có sẵn View của riêng nó là ListView rồi, ListView này có kích thước phủ toàn bộ màn hình.

ArrayAdapter<String> la = new ArrayAdapter<String>(this, R.layout.row);  
la.add("Python");
la.add("Java");
la.add("Ruby");
la.add("C++");

Chúng ta tạo đối tượng ArrayAdapter như bình thường để truyền dữ liệu vào ListView.

setListAdapter(la);

Phương thức setListAdapter() sẽ gắn dữ liệu của đối tượng Adapter với đối tượng ListView.

ListView lv = getListView();
lv.setOnItemClickListener(this);
lv.setOnItemSelectedListener(this);

Chúng ta thiết lập các listener cho ListView.

public void onItemClick(AdapterView<?> parent, View view,
    int position, long id) 
{        
    String language = ((TextView) view).getText().toString();
    setTitle(language);
}

Giao diện OnItemClickListener có phương thức trừu tượng là onItemClick(), ở đây chúng ta làm công việc là thiết lập đoạn text trên thanh tiêu đề là text của item được click.

public void onItemSelected(AdapterView<?> parent, View view,
    int position, long id) 
{        
    String language = ((TextView) view).getText().toString();
    setTitle(language);
}

public void onNothingSelected(AdapterView<?> parent) 
{        
    
}

Giao diện OnItemSelectedListener có 2 phương thức trừu tượng, tuy nhiên chúng ta chỉ code phương thức onItemSelected() thôi, ở đây chúng ta cũng chỉ đơn giản là thiết lập lại tiêu đề của ứng dụng bằng tiêu đề của item được chọn trong ListView.

Screenshot_2016-05-21-15-47-17

Android – Một số View cơ bản – Phần 1

Trong phần này chúng ta sẽ tìm hiểu một số lớp View cơ bản.

Spinner

SpinnerView cho phép chúng ta chọn một item trong một danh sách các item.

Ví dụ 1:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="vertical" 
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent" >
    <Spinner android:id="@+id/spn" 
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" 
        android:entries="@array/dlangs" 
        android:layout_marginTop="10dip" 
        android:prompt="@string/spn_title" />

   <TextView android:id="@+id/tvId" 
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:layout_marginTop="10dip" />        
        
</LinearLayout>

Trong file main.xml, chúng ta khai báo thẻ Spinner và thẻ TextView. Danh sách các item của Spinner sẽ được khai báo trong file strings.xml.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">Spinner</string>
    <string name="spn_title">Choose a language</string>
    
    <string-array name="dlangs">
        <item>Python</item>
        <item>Java</item>
        <item>C++</item>      
        <item>Ruby</item>
    </string-array>    
    
</resources>

Trong file strings.xml chúng ta khai báo tên biến spn_title dùng làm tiêu đề cho Spinner, biến dlangs có kiểu string-array là danh sách các item cho Spinner.

package com.phocode;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Spinner;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;


public class MainActivity extends Activity implements OnItemSelectedListener 
{
    private TextView tv;
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        tv = (TextView) findViewById(R.id.tvId);

        Spinner spn = (Spinner) findViewById(R.id.spn);
        spn.setOnItemSelectedListener(this);
    }

    @Override
    public void onItemSelected(AdapterView<?> parent, View v, int pos, long id) 
    {
      String item = parent.getItemAtPosition(pos).toString();
      tv.setText(item);
    }

    @Override
    public void onNothingSelected(AdapterView<?> arg0) 
    {      
      tv.setText("");
    }
}

Item được chọn trong Spinner sẽ được dùng làm text của TextView.

public class MainActivity extends Activity implements OnItemSelectedListener 

Khi có Item nào được chọn thì Spinner sẽ giải phóng sự kiện, chúng ta implement giao diện OnItemSelectedListerner để bắt lấy sự kiện đó, có 2 phương thức phải override lại là onItemSelected()onNothingSelected().

Spinner spn = (Spinner) findViewById(R.id.spn);
spn.setOnItemSelectedListener(this);

Để các phương thức trên có thể bắt sự kiện thì chúng ta dùng phương thức setOnItemSelectedListener() của Spinner và truyền vào đối tượng có override các phương thức đó.

@Override
public void onItemSelected(AdapterView<?> parent, View v, int pos, long id) 
{
    String item = parent.getItemAtPosition(pos).toString();
    tv.setText(item);
}

Trong phương thức onItemSelected() chúng ta lấy đối tượng item đang được chọn bằng phương thức onItemAtPosition(), sau đó chuyển thành String rồi gán làm text cho TextView.

Capture

Ví dụ 2

Trong ví dụ này chúng ta sẽ thiết kế lại như ví dụ trên, chỉ khác là các item sẽ được thêm vào trong Spinner từ file java chứ không nhập cứng từ file resource nữa.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="vertical" 
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent" >
    
    <Spinner android:id="@+id/spnId"
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:layout_marginTop="10dip" 
        android:prompt="@string/spn_title" />    
      
    <TextView android:id="@+id/tvId"
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:layout_marginTop="10dip" />
    
</LinearLayout>

Trong file main.xml chúng ta có 2 ViewSpinnerTextView. Spinner ở đây không được thiết lập thuộc tính src nữa.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">Spinner2</string>
    <string name="spn_title">Choose a language</string>
</resources>

Trong file strings.xml chúng ta bỏ danh sách item đi, chỉ còn biến lưu tiêu đề của Spinner.

package com.phocode;

import java.util.ArrayList;
import java.util.List;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Spinner;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;

public class MainActivity extends Activity implements OnItemSelectedListener
{
    private TextView tv;
    private Spinner spn;
    
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        tv = (TextView)findViewById(R.id.tvId);
 
        spn = (Spinner)findViewById(R.id.spn);
 
        List<String> lst = new ArrayList<String>();
        lst.add("Python");
        lst.add("Java");
        lst.add("C++");
        lst.add("Ruby");
        ArrayAdapter<String> da = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, lst);        
 
        spn.setAdapter(da);
 
        spn.setOnItemSelectedListener(this);    
    }       

    @Override
    public void onItemSelected(AdapterView<?> parent, View v, int pos, long id) 
    {
      String item = parent.getItemAtPosition(pos).toString();
      tv.setText(item);
    }

    @Override
    public void onNothingSelected(AdapterView<?> arg0) 
    {      
      tv.setText("");
    }
}

Trong file MainActivity.java chúng ta thiết lập dữ liệu cho Spinner và override các phương thức lắng nghe sự kiện như ví dụ trên.

List<String> lst = new ArrayList<String>();
lst.add("Python");
lst.add("Java");
lst.add("C++");
lst.add("Ruby");

Đầu tiên chúng ta sử dụng List để tạo một danh sách các item sẽ được lưu vào Spinner.

ArrayAdapter<String> da = new ArrayAdapter<String>(this,
        android.R.layout.simple_spinner_item, lst);

Tiêp theo chúng ta sử dụng một đối tượng ArrayAdapter, đối tượng này có tác dụng liên kết dữ liệu giữa SpinnerList<String>, mỗi khi List có sự thay đổi, chẳng hạn như thêm hoặc xóa bớt item thì Spinner cũng sẽ tự động thêm/bớt các item đó.

spn.setAdapter(da);

Có đối tượng ArrayAdapter rồi thì chúng ta phải thiết lập adapter đó cho Spinner.Capture

SeekBar

SeekBar hiển thị một thanh trượt và một cái nút trên thanh trượt đó cho phép chúng ta kéo qua kéo lại trong một khoảng giá trị số nào đó, mỗi khi nút trên thanh trượt thay đổi thì SeekBar sẽ giải phóng sự kiện và chúng ta bắt sự kiện đó bằng cách dùng giao diện OnSeekBarChangeListener.

Ví dụ:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="vertical" 
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent" >  
    <SeekBar android:id="@+id/sbId" 
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:layout_margin="10dp" 
        android:max="100" 
        android:progress="50" />
    <TextView android:id="@+id/tvId" 
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:layout_marginLeft="10dp" />     
</LinearLayout>

Ở đây chúng ta thiết kế một SeekBar và một TextView, TextView được dùng để hiển thị giá trị của SeekBar. Khoảng giá trị mặc định của SeekBar là từ 0 đến 100. Trong đó chúng ta có thể thiết lập giá trị max bằng thuộc tính android:max, tuy nhiên chúng ta không thể thiết lập giá trị min được.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">SeekBar</string>
    <string name="textview_value">50</string>
</resources>

Trong file strings.xml chúng ta khai báo biến textview_value làm giá trị khởi tạo ban đầu cho TextView.

package com.phocode;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;

public class MainActivity extends Activity implements 
    OnSeekBarChangeListener
{
    TextView tv;
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        SeekBar sb = (SeekBar) findViewById(R.id.sbId);
        sb.setOnSeekBarChangeListener(this);

        tv = (TextView) findViewById(R.id.tvId);
        String val = this.getString(R.string.textview_value);
        tv.setText(val);
    }

   @Override
   public void onProgressChanged(SeekBar seekBar, int progress,
     boolean fromUser) 
   {
       tv.setText(String.valueOf(progress));
   }

   @Override
   public void onStartTrackingTouch(SeekBar seekBar) 
   {
       
   }

   @Override
   public void onStopTrackingTouch(SeekBar seekBar) 
   {
       
   }
}

Đoạn text trong TextView dùng chung với giá trị của SeekBar.

public class MainActivity extends Activity implements 
    OnSeekBarChangeListener

Lớp MainActivity implement giao diện OnSeekBarChangeListener. Giao diện này có 3 phương thức cần phải override là onProgressChanged(), onStartTrackingTouch() và onStopTrackingTouch(). Ở đây chúng ta chỉ cần dùng đến phương thức đầu tiên.

SeekBar sb = (SeekBar) findViewById(R.id.sbId);
sb.setOnSeekBarChangeListener(this);

Chúng ta lấy đối tượng SeekBar và gắn listener cho nó là đối tượng Activity hiện tại vì đối tượng này đã implement giao diện OnSeekBarChangeListener.

tv = (TextView) findViewById(R.id.tvId);
String val = this.getString(R.string.textview_value);
tv.setText(val);

Sau đó chúng ta lấy giá trị của biến textview_value rồi dùng làm text của TextView.

@Override
public void onProgressChanged(SeekBar seekBar, int progress,
    boolean fromUser) 
{
    tv.setText(String.valueOf(progress));
}

Khi chúng ta kéo nút trên SeekBar, phương thức onProgressChanged() sẽ được gọi, giá trị thay đổi được truyền trong tham số progress, chúng ta dùng tham số này làm text của TextView.

Capture

Android – Quản lý Layout

Trong phần này chúng ta sẽ tìm hiểu một số loại layout trong Android.

Layout sẽ quy định kích thước cũng như sự sắp xếp của các View trên màn hình. Android có rất nhiều lớp layout, LinearLayout sẽ sắp xếp các View trên một hàng hoặc một cột, FrameLayout chỉ hiển thị một View, RelativeLayout sắp xếp các View theo mối quan hệ giữa chúng, GridLayout sắp xếp các View theo dạng bảng.

Hiển thị ảnh với FrameLayout

Bên trong thư mục res của project có các thư mục drawable để chúng ta đặt các file tài nguyên vào trong đó và chúng ta có thể tham chiếu đến chúng trong file layout dễ dàng. Chẳng hạn ở đây mình đặt một file ảnh với tên zamok.jpg trong thư mục drawable-hdpi.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:layout_gravity="top" 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" >
    
    <ImageView android:layout_height="match_parent" 
        android:layout_width="match_parent" 
        android:src="@drawable/zamok" />        
</FrameLayout>

Trong file main.xml, chúng ta sử dụng FrameLayout làm ViewGroup chính, bên trong FrameLayout này chúng ta đặt một thẻ ImageView.

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:layout_gravity="top" 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" >

Thuộc tính layout_gravity sẽ bố trí FrameLayout ở các vị trí khác nhau, ở đây top tức là đưa lên đầu màn hình, ngoài ra còn có các giá trị khác như bottom, left, right, center... Thuộc tính layout_widthlayout_height với giá trị wrap_content sẽ quy định kích thước layout vừa đủ để bọc lấy các thành phần bên trong nó.

<ImageView android:layout_height="match_parent" 
    android:layout_width="match_parent"
    android:src="@drawable/zamok" />

Lớp ImageView sẽ hiển thị ảnh, đường dẫn đến file ảnh được truyền vào thuộc tính android:src.

Capture

LinearLayout

Trong ví dụ dưới đây, chúng ta sẽ thiết kế một hàng các Button.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="horizontal" 
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    
  <Button 
        android:layout_height="wrap_content" 
        android:layout_width="wrap_content" 
        android:text="Button1" />

  <Button 
        android:layout_height="wrap_content" 
        android:layout_width="wrap_content" 
        android:text="Button2" />
      
  <Button 
        android:layout_height="wrap_content" 
        android:layout_width="wrap_content"
        android:text="Button3" />
      
  <Button 
        android:layout_height="wrap_content" 
        android:layout_width="wrap_content" 
        android:text="Button4" />      
      
</LinearLayout>

Lớp LinearLayout sẽ sắp xếp các View theo hàng hoặc theo cột.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="horizontal" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent" >

Thuộc tính widthheight có giá trị match_parent tức là vừa khít với kích thước của thiết bị.

<Button android:layout_height="wrap_content" 
    android:layout_width="wrap_content" 
    android:text="Button1" />

Mỗi Buttonwidthheightwrap_content, tức là kích thước của chúng vừa đủ để bọc lấy đoạn text bên trong nó.

Capture

Thiết kế bằng Java

Ngoài việc thiết kế trong file layout, chúng ta cũng có thể thiết kế trong file Activity.

package com.phocode;

import android.app.Activity;
import android.os.Bundle;
import android.widget.Button;
import android.widget.LinearLayout;

public class MainActivity extends Activity
{
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
 
        Button btn1 = new Button(this);
        btn1.setText("Button");
 
        Button btn2 = new Button(this);
        btn2.setText("Button");
 
        Button btn3 = new Button(this);
        btn3.setText("Button");
 
        Button btn4 = new Button(this);
        btn4.setText("Button");
 
        LinearLayout ll = new LinearLayout(this);
        ll.setOrientation(LinearLayout.HORIZONTAL);
 
        ll.addView(btn1);
        ll.addView(btn2);
        ll.addView(btn3);
        ll.addView(btn4);
 
        setContentView(ll);
    }
}

Ở đây chúng ta cũng thiết kế lại giống như ví dụ trước là đặt 4 Button nằm trên cùng một hàng.

Button btn1 = new Button(this);
btn1.setText("Button");

Để tạo Button thì chúng ta dùng tới lớp android.widget.Button, phương thức setText() sẽ thiết lập nội dung Button.

LinearLayout ll = new LinearLayout(this);
ll.setOrientation(LinearLayout.HORIZONTAL);

Tương tự với lớp android.widget.LinearLayout.

ll.addView(btn1);
ll.addView(btn2);
ll.addView(btn3);
ll.addView(btn4);

Phương thức addView() sẽ thêm các View vào layout.

setContentView(ll);

Phương thức setContentView() sẽ nhận đối tượng LinearLayout thay vì nhận ID của file XML như trước.

Kết hợp các Layout

Chúng ta có thể lồng các layout vào nhau để kết hợp chúng lại.

<?xml version="1.0" encoding="utf-8"?>  
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" 
    android:layout_gravity="center" >  
    
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
        android:orientation="vertical" 
        android:layout_width="match_parent" 
        android:layout_height="match_parent" >

        <Button 
            android:layout_height="wrap_content" 
            android:layout_width="wrap_content" 
            android:text="Button" />    
        
        <Button 
            android:layout_height="wrap_content" 
            android:layout_width="wrap_content" 
            android:text="Button" />    
        
        <Button 
            android:layout_height="wrap_content" 
            android:layout_width="wrap_content" 
            android:text="Button" />    
        
        <Button 
            android:layout_height="wrap_content" 
            android:layout_width="wrap_content" 
            android:text="Button" />               
    </LinearLayout>
</FrameLayout>   

Trong ví dụ này chúng ta lồng LinearLayout vào bên trong Framelayout.

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" 
    android:layout_gravity="center" > 

FrameLayout có kích thước vừa đủ để bọc lấy các phần tử bên trong nó. Các phần tử bên trong FrameLayout sẽ nằm chính giữa màn hình theo thuộc tính layout_gravitycenter.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="vertical" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent" >

LinearLayout bên trong FrameLayout chứa 4 Button được sắp xếp theo chiều dọc.

Capture

RelativeLayout

RelativeLayout sắp xếp các View dựa trên vị trí của chúng với nhau hoặc với View cha.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" >
    
    <EditText 
        android:id="@+id/etId" 
        android:layout_marginTop="10dp" 
        android:layout_width="match_parent" 
        android:layout_height="wrap_content" />  
        
    <Button 
        android:id="@+id/btn_sendId" 
        android:layout_below="@+id/etId" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:text="Send" />          
          
    <Button 
       android:id="@+id/btn_clearId" 
       android:layout_below="@+id/etId" 
       android:layout_toRightOf="@+id/btn_sendId" 
       android:layout_width="wrap_content" 
       android:layout_height="wrap_content" 
       android:text="Clear" />    
     
</RelativeLayout>  

Trong ví dụ này chúng ta hiển thị 1 EditText và 2 Button.

<EditText 
    android:id="@+id/etId" 
    android:layout_marginTop="10dp" 
    android:layout_width="match_parent" 
    android:layout_height="wrap_content" /> 

EditText sẽ có bề ngang chiếm toàn bộ màn hình, chiều cao chỉ vừa đủ bọc lấy đoạn text bên trong. Ngoài ra thuộc tính marginTop quy định EditText cách cạnh trên của màn hình 10dp.

<Button 
    android:id="@+id/btn_sendId" 
    android:layout_below="@+id/etId" 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" 
    android:text="Send" />   

Chúng ta có 2 Button, trong đó Button “Send” sẽ được đặt phía dưới EditText nhờ vào thuộc tính layout_below, thuộc tính này nhận id của EditText.

<Button 
    android:id="@+id/btn_clearId" 
    android:layout_below="@+id/etId" 
    android:layout_toRightOf="@+id/btn_sendId" 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" 
    android:text="Clear" />   

Button “Clear” được đặt phía dưới EditText và nằm phía bên phải Button “Send”, ở đây chúng ta dùng thêm thuộc tính layout_toRightOf để chỉ định View nằm phía bên phải của View nào.Capture

GridLayout

GridLayout sắp xếp các View con theo dạng bảng, bảng bao gồm nhiều hàng và cột, hàng và cột giao nhau tạo thành các ô, một View có thể chiếm một hoặc nhiều ô. Thuộc tính gravity cho biết View sẽ được đặt ở đâu trong bảng.

<?xml version="1.0" encoding="utf-8"?>
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent" >
    
    <Button 
        android:text="(0, 0)" 
        android:layout_row="0" 
        android:layout_column="0" />    
       
    <Button 
        android:layout_row="0" 
        android:layout_column="1" 
        android:layout_columnSpan="2" 
        android:layout_gravity="fill_horizontal" />       
      
    <Button 
        android:text="(0, 3)" 
        android:layout_row="0" 
        android:layout_column="3" />       
    
    <Button 
        android:text="(0, 4)" 
        android:layout_row="0" 
        android:layout_column="4" />       
      
    <Button 
        android:layout_row="1" 
        android:layout_column="0" 
        android:layout_rowSpan="3" 
        android:layout_columnSpan="5" 
        android:layout_gravity="fill" />   
      
    <Button 
        android:text="Center" 
        android:layout_row="4" 
        android:layout_column="0" 
        android:layout_columnSpan="5" 
        android:layout_gravity="center_horizontal" />      
      
    <Button 
        android:text="Right" 
        android:layout_row="5" 
        android:layout_column="0" 
        android:layout_columnSpan="5" 
        android:layout_gravity="right" />           
    
</GridLayout>

Trong ví dụ này chúng ta có một GridLayout chứa một nhóm các Button.

<Button 
    android:text="(0, 0)" 
    android:layout_row="0" 
    android:layout_column="0" />  

Thuộc tính layout_row biểu thị số hàng còn thuộc tính layout_column biểu thị số cột, ở đây hàng 0 cột 0 tức là Button này nằm ở góc trái phía trên bảng.

<Button 
    android:layout_row="0" 
    android:layout_column="1" 
    android:layout_columnSpan="2" 
    android:layout_gravity="fill_horizontal" /> 

Button ở đoạn code trên nằm ở ô (0,1) nhưng có chiều ngang dài thêm 2 ô do được thiết lập trong thuộc tính columnSpan nhưng chúng ta phải khai báo thêm thuộc tính layout_gravityfill_horizontal, nếu không thì Button này vẫn sẽ có kích thước như cũ.

<Button 
    android:layout_row="1" 
    android:layout_column="0" 
    android:layout_rowSpan="3" 
    android:layout_columnSpan="5" 
    android:layout_gravity="fill" /> 

Tương tự Button ở đoạn code trên nằm ở hàng 1 cột 0, thuộc tính layout_rowSpan quy định chiều cao của Button này dài thêm 3 hàng, thuộc tính columnSpan quy định Button này dãn kích thước theo chiều ngang thêm 5 cột nữa, thuộc tính layout_gravity là fill sẽ lấp đầy khoảng trống được dãn ra đó.

<Button 
    android:text="Center" 
    android:layout_row="4" 
    android:layout_column="0" 
    android:layout_columnSpan="5" 
    android:layout_gravity="center_horizontal" />  

Button ở đoạn code trên nằm ở giữa cột nhờ vào thuộc tính layout_gravitycenter_horizontal.

Capture

Android – Permission

Hệ điều hành Android bảo vệ chính nó và thông tin riêng của người dùng bằng cách chạy các ứng dụng Android trên một môi trường ảo riêng (thuật ngữ gọi là Sandbox), môi trường ảo này có tài nguyên riêng, không đụng chạm gì tới tài nguyên của hệ điều hành, nếu ứng dụng muốn sử dụng tài nguyên bên ngoài Sandbox thì ứng dụng phải xin permission – quyền sử dụng. Tùy thuộc vào loại tài nguyên mà ứng dụng muốn truy cập, hệ điều hành sẽ cấp quyền sử dụng tự động hoặc sẽ phải hỏi ý kiến của người dùng thì mới được sử dụng.

Khai báo Permission

Tùy vào loại tài nguyên cần sử dụng mà ứng dụng sẽ phải khai báo permission cho phù hợp. Các permission sẽ được khai báo trong file AndroidManifest.xml. Tùy vào loại dữ liệu có mức độ riêng tư đến mức nào mà hệ điều hành sẽ tự động cấp quyền hoặc phải xin ý kiến người dùng, chẳng hạn như quyền sử dụng đèn pin sẽ được cấp ngay, trong khi quyền truy cập danh sách số điện thoại liên lạc sẽ phải hỏi ý kiến người dùng… Cách người dùng cấp quyền cũng khác nhau theo từng phiên bản Android, chẳng hạn như đối với phiên bản Android 5.1 trở về trước thì người dùng sẽ cấp quyền trong quá trình cài đặt ứng dụng, còn ở phiên bản Android 6.0 trở lên thì người dùng sẽ cấp quyền khi ứng dụng đang chạy.

Xác định loại quyền cần dùng

Thông thường thì ứng dụng sẽ cần dùng đến các loại dữ liệu mà bản thân nó không thể tự tạo ra được, hay các hành động có thể làm ảnh hưởng đến hành vi của smartphone hoặc các ứng dụng khác. Chẳng hạn như quyền truy cập Internet, quyền sử dụng Camera, quyền tắt/bật Wifi…

Các quyền lại được chia làm nhiều cấp độ, trong đó 2 cấp độ cao nhất là bình thường (normal) và nguy hiểm (dangerous). Quyền bình thường là các quyền sử dụng tài nguyên mà ít có rủi ro đối với sự riêng tư của người dùng, loại quyền này sẽ được hệ điều hành tự động cấp. Dưới đây là danh sách các quyền bình thường có trong phiên bản API 23:

  • ACCESS_LOCATION_EXTRA_COMMANDS
  • ACCESS_NETWORK_STATE
  • ACCESS_NOTIFICATION_POLICY
  • ACCESS_WIFI_STATE
  • BLUETOOTH
  • BLUETOOTH_ADMIN
  • BROADCAST_STICKY
  • CHANGE_NETWORK_STATE
  • CHANGE_WIFI_MULTICAST_STATE
  • CHANGE_WIFI_STATE
  • DISABLE_KEYGUARD
  • EXPAND_STATUS_BAR
  • GET_PACKAGE_SIZE
  • INSTALL_SHORTCUT
  • INTERNET
  • KILL_BACKGROUND_PROCESSES
  • MODIFY_AUDIO_SETTINGS
  • NFC
  • READ_SYNC_SETTINGS
  • READ_SYNC_STATS
  • RECEIVE_BOOT_COMPLETED
  • REORDER_TASKS
  • REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
  • REQUEST_INSTALL_PACKAGES
  • SET_ALARM
  • SET_TIME_ZONE
  • SET_WALLPAPER
  • SET_WALLPAPER_HINTS
  • TRANSMIT_IR
  • UNINSTALL_SHORTCUT
  • USE_FINGERPRINT
  • VIBRATE
  • WAKE_LOCK
  • WRITE_SYNC_SETTINGS

Quyền nguy hiểm là quyền sử dụng các loại tài nguyên có liên quan đến sự riêng tư của người dùng hoặc có thể ảnh hưởng đến hệ điều hành và các ứng dụng khác. Loại quyền này cần được sự cho phép của người dùng. Dưới đây là danh sách các quyền nguy hiểm có trong phiên bản API 23:

  • READ_CALENDAR
  • WRITE_CALENDAR
  • CAMERA
  • READ_CONTACTS
  • WRITE_CONTACTS
  • GET_ACCOUNTS
  • ACCESS_FINE_LOCATION
  • ACCESS_COARSE_LOCATION
  • RECORD_AUDIO
  • READ_PHONE_STATE
  • CALL_PHONE
  • READ_CALL_LOG
  • WRITE_CALL_LOG
  • ADD_VOICEMAIL
  • USE_SIP
  • PROCESS_OUTGOING_CALLS
  • BODY_SENSORS
  • SEND_SMS
  • RECEIVE_SMS
  • READ_SMS
  • RECEIVE_WAP_PUSH
  • RECEIVE_MMS
  • READ_EXTERNAL_STORAGE
  • WRITE_EXTERNAL_STORAGE

Ứng dụng chỉ cần xin quyền để nó có thể sử dụng trực tiếp tài nguyên, nếu trong quá trình chạy mà ứng dụng có sử dụng dữ liệu lấy từ một ứng dụng khác thì chỉ có ứng dụng khác mới cần xin quyền.

Khai báo trong file AndroidManifest.xml

Để khai báo quyền thì chúng ta sử dụng thẻ <user-permissions> trong thẻ <manifest>. Ví dụ:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.phocode">
    <uses-permission android:name="android.permission.SEND_SMS"/>
    <application ...>
        ...
    </application>
</manifest>

Ở đây chúng ta xin quyền được gửi tin SMS. Đây là loại quyền nguy hiểm.

Xin quyền trong quá trình chạy

Đối với loại quyền nguy hiểm thì quá trình xin quyền sẽ khác ở từng phiên bản hệ điều hành và từng phiên bản SDK. Ví dụ:

  • Nếu thiết bị chạy Android phiên bản 5.1 trở xuống và ứng dụng sử dụng API 22 trở xuống thì người dùng sẽ phải tự tay cấp quyền trong quá trình cài đặt ứng dụng, nếu không cấp thì ứng dụng sẽ không được cài đặt.
  • Nếu thiết bị chạy Android 6.0 trở lên và ứng dụng sử dụng API 23 trở lên thì ứng dụng sẽ được cài nhưng khi chạy thì ứng dụng sẽ lần lượt xin từng quyền từ người dùng, người dùng có thể cấp quyền này, bỏ quyền kia và ứng dụng sẽ vẫn chạy nhưng giới hạn với những quyền không được cấp.

Kể từ phiên bản Android 6.0 (API 23), người dùng có thể lấy lại quyền của ứng dụng, ví dụ như chúng ta có một ứng dụng cần sử dụng camera, và hôm nay người dùng đã cho phép quyền sử dụng camera thì không có nghĩa là ngày hôm sau ứng dụng vẫn còn có quyền đó, do đó trước khi chạy chúng ta nên kiểm tra xem ứng dụng của chúng ta có quyền hay không đã.

Ví dụ

Chúng ta viết một ứng dụng cần có quyền đọc danh sách số điện thoại liên lạc.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" 
    package="com.phocode.sharedpreferences">

    <uses-permission android:name="android.permission.READ_CONTACTS"/>
    <application android:allowBackup="true" 
        android:icon="@mipmap/ic_launcher" 
        android:label="@string/app_name" 
        android:supportsRtl="true" 
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

Trong file AndroidManifest.xml chúng ta khai báo quyền trong thẻ <uses-permission>. 

package com.phocode;

import android.Manifest;
import android.content.pm.PackageManager;
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.widget.Toast;

public class MainActivity extends Activity {

    private static final int MY_PERMISSIONS_REQUEST_READ_CONTACTS = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) 
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) 
                                != PackageManager.PERMISSION_GRANTED) 
        {
            ActivityCompat.requestPermissions(this, 
                                new String[]{Manifest.permission.READ_CONTACTS}, 
                                READ_CONTACTS_CODE);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
        switch (requestCode) {
            case READ_CONTACTS_CODE: 
            {
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    Toast.makeText(getApplicationContext(), "Contacts permission granted", Toast.LENGTH_SHORT).show();
                } 
                else 
                {                   
                    Toast.makeText(getApplicationContext(), "Contacts permission denied", Toast.LENGTH_SHORT).show();
                }
                return;
            }
        }
    }
}

Trong file MainActivity.java chúng ta thực hiện các công việc xin quyền đọc danh sách liên lạc.

private static final int READ_CONTACTS_CODE = 1;

Hằng số READ_CONTACTS_CODE là một hằng số do chúng ta tự định nghĩa, hằng số này có tác dụng giống như ID để phân biệt các lần xin quyền.

if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) 
                                != PackageManager.PERMISSION_GRANTED) 

Phương thức checkSelfPermission() của lớp android.support.v4.ContextCompat sẽ kiểm tra xem ứng dụng đã được cấp quyền đó rồi hay chưa, lớp này được định nghĩa trong thư viện support v4. Chúng ta sẽ tìm hiểu về các thư viện support này sau.

ActivityCompat.requestPermissions(this, 
                                  new String[]{Manifest.permission.READ_CONTACTS}, 
                                  READ_CONTACTS_CODE);

Nếu quyền chưa được cấp thì chúng ta xin quyền bằng cách gọi phương thức ActivityCompat.requestPermission(), phương thức này nhận vào đối tượng Activity hiện tại, danh sách các quyền trong một mảng String và ID mà chúng ta đã định nghĩa ở trên, kết quả trả về là PackageManager.PERMISSION_GRANTED nếu được chấp nhận, ngược lại là PackageManager.PERMISSION_DENIED.

@Override
public void onRequestPermissionsResult(int requestCode, 
                                       String permissions[], 
                                       int[] grantResults) 
{
...
}

Phương thức requestPermission() sẽ hiển thị một hộp thoại xin cấp quyền cho người dùng, kết quả trả về sẽ được truyền vào lời gọi phương thức onRequestPermission(), phương thức này nhận ID của quyền được xin, danh sách các quyền trong mảng permissions và danh sách kết quả của từng quyền trong mảng grantResults.

switch (requestCode) {
    case READ_CONTACTS_CODE: 
    {
    ...
    }

Câu lệnh switch() sẽ thực thi từng câu lệnh tương ứng với từng ID.

if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) 
{
    Toast.makeText(getApplicationContext(), "Contacts permission granted", Toast.LENGTH_SHORT).show();
} 
else 
{   
    Toast.makeText(getApplicationContext(), "Contacts permission denied", Toast.LENGTH_SHORT).show();
}

Ở đây chúng ta chỉ xin cấp 1 quyền là READ_CONTACTS, do đó các mảng trả về chỉ có 1 phần tử, chúng ta kiểm tra xem kết quả xin quyền READ_CONTACTS có thành công hay không bằng cách so sánh với hằng số PERMISSION_GRANTED. Nếu có thì hiện một câu thông báo thành công, ngược lại thì báo thất bại.

Nếu ứng dụng đã từng bị từ chối cấp quyền bởi người dùng thì các lần xin quyền tiếp theo hộp thoại sẽ hiện một checkbox đề “Never ask again”, nếu người dùng check vào thì ứng dụng sẽ không bao giờ có thể xin quyền được nữa.

Capture

Android – Giao tiếp với ứng dụng khác – Phần 2

Khi chúng ta chạy một Activity khác thì chúng ta không những có thể chạy được Activity đó mà còn có thể nhận những giá trị trả về của Activity đó, chẳng hạn như chúng ta mở Camera và nhận về ảnh chụp từ Camera đó, hay mở danh sách số điện thoại liên lạc và lấy về danh sách đó…v.v. Để làm được điều này thì chúng ta sẽ cần phải sử dụng đến phương thức startActivityForResult() thay vì dùng phương thức startActivity(), khi người dùng hoàn tất công việc và tắt Activity thì dữ liệu sẽ được gửi về qua phương thức onActivityResult(), chúng ta phải override phương thức này.

Ví dụ

Chúng ta sẽ viết ứng dụng lấy số điện thoại của một item trong danh sách liên lạc.

Đầu tiên chúng ta thiết kế main layout đơn giản như thế này:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    xmlns:tools="http://schemas.android.com/tools" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent" 
    tools:context="com.phocode.MainActivity">
    
    <TextView android:id="@+id/textView" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" />
</RelativeLayout>

Layout này chỉ có một TextView làm nhiệm vụ hiển thị số điện thoại sau khi đã chọn xong.

Tiếp theo là file Activity:

package com.phocode;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import android.app.Activity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;

public class MainActivity extends Activity {

    private static final int PICK_CONTACT_REQUEST = 1;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Intent pickContactIntent = new Intent(Intent.ACTION_PICK, Uri.parse("content://contacts"));
        pickContactIntent.setType(ContactsContract.CommonDataKinds.Phone.CONTENT_TYPE);
        startActivityForResult(pickContactIntent, PICK_CONTACT_REQUEST);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data)
    {
        if(requestCode == PICK_CONTACT_REQUEST)
            if(resultCode == RESULT_OK)
            {
                Uri contactUri = data.getData();
                String[] projection = {ContactsContract.CommonDataKinds.Phone.NUMBER};

                Cursor cursor = getContentResolver().query(contactUri, projection, null, null, null);
                cursor.moveToFirst();

                TextView tv = (TextView)findViewById(R.id.textView);

                int column = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
                String number = cursor.getString(column);

                tv.setText(number);
            }
    }
}

Ở đây chúng ta mở một Activity mới bằng phương thức startActivityForResult().

private static final int PICK_CONTACT_REQUEST = 1;
...
startActivityForResult(pickContactIntent, PICK_CONTACT_REQUEST);

Phương thức startActivityForResult() ngoài việc nhận một đối tượng Intent còn nhận thêm một tham số là một số nguyên, tham số này sẽ được gán vào đối tượng Intent và có tác dụng giống như một ID dành cho mỗi đối tượng Intent vậy, bởi vì nhiều khi ứng dụng của chúng ta có thể có nhiều đối tượng Intent khác nhau nên chúng ta cần một ID để phân biệt các đối tượng Intent.

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
   ...
}

Khi người dùng chọn xong một số điện thoại liên lạc và màn hình Activity của danh sách liên lạc tắt đi thì ứng dụng sẽ gọi đến phương thức onActivityResult() và truyền vào các tham số là các kết quả trả về của Activity đó, ở đây có 3 tham số là requestCode tức là ID mà chúng ta đã khai báo ở trên, resultCode là kết quả của hành động mà người dùng đã thực hiện (có 2 giá trị là RESULT_OK tức là người dùng chọn thành công và RESULT_CANCELED tức là thất bại, chẳng hạn như người dùng bấm nút lùi, tắt máy…v.v), tham số data là dữ liệu mà người dùng đã chọn.

if(requestCode == PICK_CONTACT_REQUEST)
    if(resultCode == RESULT_OK)
    {
        Uri contactUri = data.getData();
        String[] projection = {ContactsContract.CommonDataKinds.Phone.NUMBER};

        Cursor cursor = getContentResolver().query(contactUri, projection, null, null, null);
        cursor.moveToFirst();

        TextView tv = (TextView)findViewById(R.id.textView);

        int column = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
        String number = cursor.getString(column);

        tv.setText(number);
    }

Để có thể xử lý dữ liệu trả về thì bạn phải biết định dạng của dữ liệu đó. Chẳng hạn như định dạng của một danh sách liên lạc sẽ khác với định dạng của một tấm ảnh trả về bởi camera. Trong đoạn code trên chúng ta kiểm tra ID và kết quả trả về rồi lấy dữ liệu gán vào TextView. Việc lấy dữ liệu được thực hiện thông qua các ContentProvider, chúng ta sẽ tìm hiểu về chủ đề này trong các bài sau.

Screenshot_2016-05-16-13-10-18

Chạy ứng dụng, ban đầu ứng dụng sẽ chạy Activity danh sách liên lạc, khi chúng click vào một số điện thoại nào thì số điện thoại đó sẽ được gửi tới Activity chính của chúng ta.

Screenshot_2016-05-16-13-10-33

Android – Giao tiếp với ứng dụng khác – Phần 1

Như đã biết thông thường một ứng dụng Android sẽ có nhiều Activity, mỗi Activity chịu trách nhiệm vẽ màn hình giao diện người dùng và xử lý các sự kiện diễn ra trên giao diện đó, chẳng hạn như vẽ bản đồ, chụp ảnh…v.v Để có thể chuyển đổi qua lại giữa các Activity thì chúng ta dùng lớp Intent (tiếng Anh có nghĩa là dự định, tức là dự định làm một cái gì đó), Intent không những cho phép chúng ta gọi tới các Activity trong cùng một ứng dụng mà còn cho phép gọi tới những Activity ở các ứng dụng khác nữa.

Mở một ứng dụng khác

Chúng ta đã từng sử dụng lớp Intent để di chuyển qua lại giữa các Activity, làm như thế là chúng ta đã gọi Intent một cách tường minh, tức là khai báo rõ ràng tên lớp Activity sẽ được mở bởi Intent. Còn đôi khi chúng ta muốn mở một ứng dụng khác thì lúc đó chúng ta sử dụng Intent “ngầm”.

Mở Intent ngầm

Các đối tượng Intent ngầm sẽ không nhận tên của một lớp Activity nào cả, mà thay vào đó là một hành động nào nó, chẳng hạn như mở ứng dụng bản đồ, mở trình duyệt, mở ứng dụng gửi mail… và các đối tượng Intent này cũng có thể gửi dữ liệu đến các hành động đó nữa, ví dụ như mở bản đồ tại thành phố Nội, mở trình duyệt tại địa chỉ phocode.com…v.v Thường thì dữ liệu gửi đi sẽ được lưu trong lớp android.net.Uri.

Ví dụ 1:

package com.phocode;

import android.content.Intent;
import android.net.Uri;
import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Uri number =  Uri.parse("tel:00841218749385");
        Intent callIntent = new Intent(Intent.ACTION_DIAL, number);
        startActivity(callIntent);
    }
}

Đoạn code trên sẽ mở màn hình gọi điện với số điện thoại được chỉ định trong đối tượng Uri.

Capture

Ví dụ 2:

package com.phocode;

import android.content.Intent;
import android.net.Uri;
import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Uri webpage = Uri.parse("https://phocode.com");
        Intent webIntent = new Intent(Intent.ACTION_VIEW, webpage);
        startActivity(webIntent);
    }
}

Đoạn code trên sẽ mở trình duyệt web mặc định tới website có địa chỉ được chỉ định trong đối tượng Uri.

Capture

Tham số khởi tạo đối tượng Intent ở đây không phải là tên một lớp Activity nào, mà là một hằng số chỉ định một hành động nào đó, ví dụ như Intent.ACTION_VIEW là mở trình duyệt web, Intent.ACTION_DIAL là mở trình gọi điện… tham số thứ 2 là một đối tượng Uri mang theo dữ liệu cho hành động đó. Danh sách các đối tượng hành động cùng với kiểu dữ liệu Uri có thể xem ở đây.

Một số đối tượng hành động có thể nhận thêm nhiều dữ liệu chứ không chỉ có một, dữ liệu đó không bắt buộc nhưng có thể truyền đi, lúc đó chúng ta truyền vào bằng phương thức putExtra() như thường, ví dụ:

package com.phocode;

import android.content.Intent;
import android.net.Uri;
import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Intent emailIntent = new Intent(Intent.ACTION_SEND);
        emailIntent.setType("text/plain");
        emailIntent.putExtra(Intent.EXTRA_EMAIL, "admin@example.com");
        emailIntent.putExtra(Intent.EXTRA_SUBJECT, "Hello");
        emailIntent.putExtra(Intent.EXTRA_TEXT, "Nice to meet you");
        startActivity(emailIntent);
    }
}

Đoạn code trên sẽ mở ứng dụng gửi tin nhắn.

Nếu hệ điều hành thấy có nhiều hơn một ứng dụng có thể mở được đối tượng Intent này thì hệ điều hành sẽ mở một hộp thoại chứa danh sách các ứng dụng đó cho người dùng chọn, còn nếu chỉ có một ứng dụng có thể mở được thì hệ điều hành sẽ chạy ứng dụng đó luôn.

Capture

Kiểm tra Intent có thể mở được hay không

Hệ điều hành Android đảm bảo rằng luôn luôn sẽ có ứng dụng được cài sẵn có thể mở được dữ liệu được gửi đi bởi đối tượng Intent ngầm. Tuy nhiên chúng ta nên kiểm tra trước thì sẽ tốt hơn bởi vì nếu không có ứng dụng nào có thể mở được đối tượng Intent của chúng ta thì chương trình của bạn sẽ bị crash (tức là bị tắt mà không rõ lý do).

Để biết được đối tượng Intent mà chúng ta định gọi có thể mở được bởi ứng dụng nào đó hay không thì chúng ta dùng phương thức queryIntentActivities() của lớp android.content.pm.PackageManager và truyền vào đối tượng Intent và chế độ kiểm tra. Ví dụ:

package com.phocode;

import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.app.Activity;
import android.os.Bundle;
import android.widget.Toast;

import java.util.List;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Uri webpage = Uri.parse("https://phocode.com");
        Intent webIntent = new Intent(Intent.ACTION_VIEW, webpage);

        PackageManager packageManager = getPackageManager();
        List activities = packageManager.queryIntentActivities(webIntent, PackageManager.MATCH_DEFAULT_ONLY);
        boolean isIntentSafe = activities.size() > 0;

        Toast.makeText(getApplicationContext(), (isIntentSafe ? "Intent is safe" : "Intent is not safe"), Toast.LENGTH_LONG).show();
    }
}

Chúng ta có thể lấy đối tượng PackageManager từ phương thức getPackageManager() của lớp Activity.

List activities = packageManager.queryIntentActivities(webIntent, PackageManager.MATCH_DEFAULT_ONLY);
boolean isIntentSafe = activities.size() > 0;

Phương thức queryIntentActivities() sẽ trả về một đối tượng List chứa danh sách các lớp Activity có thể mở được đối tượng Intent của chúng ta. Ở đây chúng ta chỉ cần xem nếu danh sách này khác rỗng thì tức là đối tượng Intent của chúng ta có thể mở được

Mở hộp thoại chọn ứng dụng

Ở ví dụ gửi tin nhắn ở trên chúng ta thấy có 2 ứng dụng có thể mở đối tượng Intent của chúng ta nên hệ điều hành sẽ hiển thị một hộp thoại cho phép người dùng lựa chọn ứng dụng để mở, ngoài ra ở dưới hộp thoại còn có 2 nút là Just OnceAlways, nút Just Once có nghĩa là ngay tại thời điểm đó chỉ chọn ứng dụng đó để sử dụng, còn các lần sau thì hệ điều hành sẽ mở lại hộp thoại để người dùng chọn ứng dụng khác, còn nút Always tức là từ nay về sau hệ điều hành sẽ sử dụng ứng dụng đó luôn chứ không bắt người dùng phải chọn lại nữa.

Tuy nhiên nếu muốn chúng ta có thể yêu cầu hệ điều hành mỗi lần chạy sẽ phải mở hộp thoại chọn ứng dụng chứ không sử dụng ứng dụng được gắn mác Always. 

Để làm việc này chúng ta tạo một đối tượng Intent và gọi phương thức createChooser() rồi truyền vào phương thức này đối tượng Intent mà chúng ta muốn sử dụng. Ví dụ:

package com.phocode;

import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.app.Activity;
import android.os.Bundle;
import android.widget.Toast;

import java.util.List;

public class MainActivity extends Activity{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Intent emailIntent = new Intent(Intent.ACTION_SEND);
        emailIntent.setType("text/plain");
        emailIntent.putExtra(Intent.EXTRA_EMAIL, "admin@example.com");
        emailIntent.putExtra(Intent.EXTRA_SUBJECT, "Hello");
        emailIntent.putExtra(Intent.EXTRA_TEXT, "Nice to meet you");
        startActivity(emailIntent);

        String title = "Choose app to open";
        Intent chooser = Intent.createChooser(emailIntent, title);

        if(emailIntent.resolveActivity(getPackageManager()) != null)
            startActivity(chooser);
    }
}

Phương thức createChooser() sẽ nhận đối tượng Intent mà chúng ta muốn mở và một chuỗi hiển thị tiêu đề cho hộp thoại.

if(emailIntent.resolveActivity(getPackageManager()) != null)
    startActivity(chooser);

Đoạn code trên chỉ là kiểm tra xem đối tương Intent của chúng ta có ứng dụng nào có thể mở được hay không thôi, nếu có thì mới cho hiển thị hộp thoại chọn ứng dụng.

Capture

Hộp thoại chọn ứng dụng theo cách này sẽ không có 2 nút Just OnceAlways.

Android – Cơ sở dữ liệu SQLite

Khi dữ liệu cần lưu trữ lớn và thường là có quy tắc chung, chẳng hạn như thông tin user… thì chúng ta nên lưu vào các hệ thống cơ sở dữ liệu. Trong bài này chúng ta sẽ tìm hiểu về cơ sở dữ liệu SQLite có trong Android.

Định nghĩa mô hình dữ liệu

Một trong những thành phần chủ chốt của một cơ sở dữ liệu SQL là các bảng, nói một cách lý thuyết thì các bảng này định nghĩa cách mà dữ liệu được tổ chức trong cơ sở dữ liệu. Chúng phản ánh chính xác các câu lệnh SQL mà bạn dùng để tạo các bảng cơ sở dữ liệu.

Bên cạnh các bảng trong cơ sở dữ liệu, chúng ta sẽ tạo các lớp mô hình và các lớp điều khiểncác lớp mô hình sẽ  mô phỏng lại cột trong các bảng còn các lớp điều khiển cung cấp các phương thức để thực hiện các thao tác trên các bảng đó. Nếu bạn đã từng học mô hình MVC (Model-View-Controller) thì các lớp này là phần Model và Controller trong mô hình này.

Một trong những cách thiết kế các lớp mô hình là định nghĩa một lớp đại diện cho toàn bộ cơ sở dữ liệu, sau đó định nghĩa các lớp nội (inner class) bên trong lớp chính đó để đại diên cho từng bảng.

Android có lớp interface android.provider.BaseColumns hỗ trợ định nghĩa bảng, điểm đặc biệt của lớp này là có sẵn một thuộc tính có tên là _ID cho bạn, tất nhiên bạn cũng không nhất thiết phải implement lớp interface này. Ví dụ:

package com.phocode;

import android.provider.BaseColumns;

public final class BlogDatabase {
    public BlogDatabase() {}

    public static abstract class EntryTable implements BaseColumns
    {
        public static final String TABLE_NAME = "Entry";
        public static final String COLUMN_TITLE = "title";
        public static final String COLUMN_DATE = "date";
        public static final String COLUMN_CONTENT = "content";
    }
}

Thao tác với cơ sở dữ liệu

Sau khi đã định nghĩa các lớp mô hình, chúng ta sẽ định nghĩa các lớp điều khiển có phương thức thực hiện các thao tác truy vấn với cơ sở dữ liệu như INSERT, UPDATE, DELETE…

Chúng ta có thể định nghĩa một vài câu truy vấn mẫu bên trong các lớp mô hình, ví dụ:

package com.phocode;

import android.provider.BaseColumns;

public final class BlogDatabase {
    public BlogDatabase() {}

    public static abstract class EntryTable implements BaseColumns
    {
        public static final String TABLE_NAME = "Entry";
        public static final String COLUMN_TITLE = "title";
        public static final String COLUMN_DATE = "date";
        public static final String COLUMN_CONTENT = "content";

        private static final String COMMA = ",";
        private static final String TYPE_INT = "INTEGER";
        private static final String TYPE_DATETIME = "DATETIME";
        private static final String TYPE_TEXT = "TEXT";
        private static final String QUERY_CREATE_RECORD = "" +
                "CREATE TABLE " + TABLE_NAME + "(" +
                _ID + " INTEGER PRIMARY KEY" + COMMA +
                COLUMN_TITLE + TYPE_TEXT + COMMA +
                COLUMN_DATE + TYPE_DATETIME + COMMA +
                COLUMN_CONTENT + TYPE_TEXT +
                ")";

        private static final String QUERY_DELETE_RECORD = "" +
                "DROP TABLE IF EXISTS " + TABLE_NAME;
    }
}

Những câu truy vấn ví dụ ở trên có thể khác tùy thuộc vào cơ sở dữ liệu mà bạn dùng. Mặc định dữ liệu trong cơ sở dữ liệu sẽ được lưu trong bộ nhớ Internal, tức là chỉ có ứng dụng của chúng ta mới có thể truy cập vào các dữ liệu này.

Android cung cấp lớp android.database.sqlite.SQLiteOpenHelper  có các phương thức thực hiện việc mở kết nối đến cơ sở dữ liệu, chạy các câu truy vấn… Thường thì chúng ta sẽ định nghĩa một lớp kế thừa từ lớp này và override 3 phương thức bắt buộc là phương thức khởi tạo và 2 phương thức onCreate()onUpgrade(). Ví dụ chúng ta định nghĩa lại lớp BlogDatabase như sau:

package com.phocode;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.provider.BaseColumns;

public final class BlogDatabase {
    public BlogDatabase() {}

    public static class EntryTableHelper extends SQLiteOpenHelper
    {
        public static final String TABLE_NAME = "Entry";
        public static final String COLUMN_ID = "id";
        public static final String COLUMN_TITLE = "title";
        public static final String COLUMN_DATE = "date";
        public static final String COLUMN_CONTENT = "content";

        private static final String COMMA = ",";
        private static final String TYPE_INT = "INTEGER";
        private static final String TYPE_DATETIME = "DATETIME";
        private static final String TYPE_TEXT = "TEXT";
        private static final String QUERY_CREATE_RECORD = "" +
                "CREATE TABLE " + TABLE_NAME + "(" +
                COLUMN_ID + " INTEGER PRIMARY KEY" + COMMA +
                COLUMN_TITLE + TYPE_TEXT + COMMA +
                COLUMN_DATE + TYPE_DATETIME + COMMA +
                COLUMN_CONTENT + TYPE_TEXT +
                ")";

        private static final String QUERY_DELETE_TABLE = "" +
                "DROP TABLE IF EXISTS " + TABLE_NAME;
        
        public static final int DATABASE_VERSION = 1;
        public static final String DATABASE_NAME = "BlogDatabase.db";

        public EntryTableHelper(Context context)
        {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }

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

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
        {
            db.execSQL(QUERY_DELETE_TABLE);
            onCreate(db);
        }
    }
}

Trong đoạn code trên chúng ta gộp phần định nghĩa các cột trong bảng và các phương thức thao tác với trong một lớp luôn và lớp này kế thừa từ lớp SQLiteOpenHelper.

Khi cần thực hiện thao tác với bảng chúng ta chỉ cần tạo một đối tượng từ lớp này và truyền vào một đối tượng Context, chúng ta có thể lấy từ phương thức Activity.getApplicationContext(). 

public EntryTableHelper(Context context)
{
    super(context, DATABASE_NAME, null, DATABASE_VERSION);
}

Bên trong hàm khởi tạo chúng ta gọi đến phương thức khởi tạo của lớp cha, phương thức này nhận vào một đối tượng Context, tên file cơ sở dữ liệu, ở đây là file BlogDatabase.db, nếu file này chưa tồn tại thì lớp này sẽ tạo một file mới, tiếp theo là một đối tượng SQLiteDatabase.CursorFactory, chúng ta có thể truyền vào null vì ở đây cũng chưa cần đến, và cuối cùng số phiên bản cơ sở dữ liệu, số phiên bản ở đây chẳng qua là các con số chúng ta dùng để phân biệt các cơ sở dữ liệu mà chúng ta đã định nghĩa chứ không phải số phiên bản CSDL SQLite hay cái gì đó tương tự.

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

Phương thức onCreate() tự động được gọi khi cần, phương thức này nhận vào một đối tượng SQLiteDatabase và thực hiện tạo bảng. Để thực hiện bất kì câu truy vấn nào thì chúng ta chỉ cần gọi đến phương thức execSQL() và đưa vào một chuỗi truy vấn là được.

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
{
    db.execSQL(QUERY_DELETE_TABLE);
    onCreate(db);
}

Phương thức onUpgrade() được gọi khi muốn cập nhật lại định nghĩa về bảng, chẳng hạn như thêm bảng mới, thêm/xóa cột…

Tạo mới bản ghi

Để tạo mới các bản ghi vào cơ sở dữ liệu thì chúng ta tạo đối tượng android.content.ContentValues rồi truyền vào phương thức SQLiteDatabase.insert(). Ví dụ:

package com.phocode;

import android.content.ContentValues;
import android.app.Activity;
import android.os.Bundle;

import java.text.SimpleDateFormat;
import java.util.Date;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        BlogDatabase.EntryTableHelper helper = new BlogDatabase.EntryTableHelper(this.getApplicationContext());

        ContentValues values = new ContentValues();
        values.put(helper.COLUMN_ID, 1);
        values.put(helper.COLUMN_TITLE, "About PhoCode");
        values.put(helper.COLUMN_DATE, new SimpleDateFormat("dd/mm/yyyy hh:mm:ss").format(new Date()));
        values.put(helper.COLUMN_CONTENT, "PhoCode - Open source is the future");

        helper.getWritableDatabase().insert(helper.TABLE_NAME, null, values);
    }
}

Phương thức getWritableDatabase() sẽ trả về một đối tượng SQLiteDatabase, từ đó chúng ta có thể gọi phương thức insert() để tạo mới một bản ghi trong bảng. Phương thức này nhận tên bảng, danh sách các cột cho phép nhận giá trị NULL, chúng ta có thể truyền vào “null” và phương thức này sẽ không tạo mới bản ghi vào mà có cột rỗng, tham số thứ 3 của phương thức insert() là đối tượng ContentValues.

Đọc bản ghi từ cơ sở dữ liệu

Để lấy các bản ghi thì chúng ta sử dụng phương thức rawQuery() của đối tượng SQLiteDatabase từ phương thức SQLiteOpenHelper.getReadableDatabase(), phương thức rawQuery() sẽ trả về một đối tượng android.database.Cursor, ví dụ:

BlogDatabase.EntryTableHelper helper = new BlogDatabase.EntryTableHelper(getApplicationContext());
SQLiteDatabase db = helper.getReadableDatabase();

Cursor cursor = db.rawQuery("SELECT * FROM " + helper.TABLE_NAME, null);

Phương thức rawQuery() nhận vào 2 tham số là một câu lệnh SQL và một chuỗi điều kiện, chuỗi điều kiện này chính là mệnh đề WHERE trong câu truy vấn, ở đoạn code trên chúng ta không có điều kiện nào cả nên để null.

Các bản ghi được trả về sẽ được lưu trong một đối tượng Cursor. Đối tượng Cursor lưu các bản ghi theo dạng danh sách và trong đối tượng này có một biến con trỏ, con trỏ này sẽ trỏ tới từng bản ghi trong danh sách đó. Chúng ta có thể lấy dữ liệu của từng cột trong từng bản ghi theo chỉ số hoặc theo tên cột, ví dụ:

if(cursor != null)
{
    if(cursor.moveToFirst())
        do {
            int id = cursor.getInt(0);
            String title = cursor.getString(cursor.getColumnIndex(helper.COLUMN_TITLE));
            String date = cursor.getString(cursor.getColumnIndex(helper.COLUMN_DATE));
            String content = cursor.getString(cursor.getColumnIndex(helper.COLUMN_CONTENT));
        } while(cursor.moveToNext());
}

Để phòng trường hợp phương thức query() không trả về bản ghi nào, chúng ta nên kiểm tra xem đối tượng Cursor có bằng null không trước.

if(cursor.moveToFirst())

Phương thức moveToFirst() sẽ chuyển con trỏ về dòng đầu tiên trong danh sách.

while(cursor.moveToNext());

Phương thức moveToNext() sẽ chuyển con trỏ đến dòng tiếp theo trong danh sách.

int id = cursor.getInt(0);
String title = cursor.getString(cursor.getColumnIndex(helper.COLUMN_TITLE));

Tại mỗi dòng, chúng ta có thể lấy giá trị của từng cột trong đó bằng các phương thức như getInt(), getFloat(), getString()... các phương thức này nhận vào chỉ số cột trong bảng, các cột này được đánh thứ tự từ 0. Nếu không biết số thứ tự của cột nào thì có thể dùng phương thức getColumnIndex(), phương thức này nhận vào tên cột và trả về số thứ tự của cột đó.

Xóa bản ghi

Để xóa bản ghi thì chúng ta dùng phương thức delete() của đối tượng SQLiteDatabase lấy từ phương thức SQLiteOpenHelper.getWritableDatabase(). Ví dụ:

BlogDatabase.EntryTableHelper helper = new BlogDatabase.EntryTableHelper(getApplicationContext());
SQLiteDatabase db = helper.getWritableDatabase();

String whereClause = "id = ? AND date <= ?";
String[] arguments = { "1", "01-01-2016 12:00:00" };
db.delete(helper.TABLE_NAME,
        whereClause,
        arguments);

Phương thức delete() nhận vào 3 tham số, tham số đầu tiên là tên bảng, tham số thứ 2 là chuỗi mệnh đề WHERE có các tham số, tham số thứ 3 là mảng các tham số truyền vào tham số thứ 2.

Trong đoạn code trên, tham số thứ 2 là "id = ? AND date <= ?", còn tham số thứ 3 là { "1", "01-01-2016" }, từ 2 tham số này Android sẽ xây dựng nên câu truy vấn như sau:

DELETE From Entry WHERE id = 1 AND date <= '01-01-2016 12:00:00'

Tức là các dấu chấm hỏi "?" trong tham số thứ 2 sẽ lần lượt được thay thế bằng các giá trị trong tham số thứ 3.

Lý do tại sao Android lại chia mệnh đề WHERE làm 2 tham số là để phòng chống kỹ thuật tấn công SQL Injection, nếu muốn bạn có thể tìm hiểu thêm trên Google.

Cập nhật bản ghi

Để cập nhật bản ghi thì chúng ta dùng phương thức update() của lớp SQLiteDatabase lấy từ phương thức SQLiteOpenHelper.getReadableDatabase(). Ví dụ:

BlogDatabase.EntryTableHelper helper = new BlogDatabase.EntryTableHelper(getApplicationContext());
SQLiteDatabase db = helper.getReadableDatabase();

ContentValues values = new ContentValues();
values.put(helper.COLUMN_TITLE, "PhoCode");
values.put(helper.COLUMN_DATE, new SimpleDateFormat("dd/mm/yyyy hh:mm:ss").format(new Date()));
values.put(helper.COLUMN_CONTENT, "Android Programming");

String whereClause = "id = ?";
String[] arguments = { "1" };

db.update(helper.TABLE_NAME, values, whereClause, arguments);

Phương thức update() nhận vào tên bảng, đối tượng ContentValues chứa các cột được cập nhật và giá trị mới của chúng, mệnh đề WHERE và tham số cho mệnh đề WHERE như với phương thức delete() ở trên.

Android – Lưu trữ dữ liệu trên file

Hầu hết khi làm ứng dụng chúng ta đều phải lưu lại dữ liệu để dùng cho các lần làm việc sau này, chẳng hạn như lưu thông tin của người dùng, lưu trạng thái của ứng dụng… Có 3 cách lưu trữ dữ liệu đó là:

  • Lưu dưới dạng từ điển trong các file ưu tiên
  • Lưu file trong thẻ nhớ
  • Lưu trong cơ sở dữ liệu SQLite

Trong phần này chúng ta sẽ tìm hiểu về 2 cách đầu tiên. Trong phần sau chúng ta sẽ tìm hiểu về cách lưu dữ liệu với cơ sở dữ liệu SQLite.

Lưu dưới dạng từ điển trong các file ưu tiên

Các file ưu tiên ở đây là các file nằm trong thư mục shared-refs, thư mục này nằm trong thư mục cài đặt ứng dụng trên máy của bạn.

Nếu dữ liệu bạn lưu lại không lớn lắm thì bạn nên sử dụng cách này. Ở đây chúng ta sẽ dùng lớp SharedPreferences để lưu trữ dữ liệu, đối tượng SharedPreferences trỏ tới một file XML chứa dữ liệu là các cặp khóa-giá trị và cung cấp các phương thức để đọc và ghi dữ liệu trên file này.

Tạo đối tượng SharedPreferences

Để tạo một đối tượng SharedPreferences thì chúng ta dùng một trong 2 phương thức:

  • getSharedPreferences() — phương thức này sẽ trả về một đối tượng SharedPreferences trỏ tới một file do chúng ta chỉ định.
  • getPreferences() — mặc định ứng dụng đã có sẵn một file riêng để lưu dữ liệu, phương thức này sẽ trả về đối tượng SharedPreferences trỏ tới file đó.

Chúng ta sẽ dùng phương thức getSharedPreferences() khi muốn lưu dữ liệu trong nhiều file khác nhau, phương thức này nhận vào tên file và chế độ đọc. Ví dụ:

package com.phocode;

import android.content.SharedPreferences;
import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity
{
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
 
        SharedPreferences sharedPref = getSharedPreferences("save", Context.MODE_PRIVATE);    
    }
}

Chế độ MODE_PRIVATE tức là file này chỉ có thể mở được từ ứng dụng của chúng ta. File save.xml sẽ được tạo ra nếu chưa có.

Còn nếu chúng ta không cần lưu vào nhiều file khác nhau thì có thể dùng file mặc định của ứng dụng bằng cách dùng phương thức getPreferences():

SharedPreferences pref = getPreferences(Context.MODE_PRIVATE);

Ghi dữ liệu lên đối tượng SharedPreferences

Để ghi dữ liệu lên các file ưu tiên này thì chúng ta tạo một đối tượng SharedPreferences.Editor từ phương thức SharedPreferences.edit().

Lớp SharedPreferences cung cấp các phương thức ghi các kiểu dữ liệu thông thường như putInt() để ghi một giá trị số nguyên, putFloat() ghi số thực, putString() ghi chuỗi... các phương thức này nhận vào khóa kèm theo giá trị. Sau khi gọi các phương thức này chúng ta gọi phương thức commit() thì dữ liệu mới thực sự được ghi vào file. Ví dụ:

package com.phocode;

import android.content.Context;
import android.content.SharedPreferences;
import android.app.Activity;
import android.os.Bundle;
import android.widget.Toast;

public class MainActivity extends Activity
{
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
 
        SharedPreferences pref = getPreferences(Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sharedPref.edit();
        editor.putString("Username", "phocode");
        editor.commit();
    }
}

Trong đoạn code trên chúng ta lưu một khóa có tên là Username với giá trị là phocode vào file ưu tiên có sẵn trong ứng dụng.

Đọc dữ liệu từ đối tượng SharedPreferences

Để đọc dữ liệu thì chúng ta gọi các phương thức như getInt(), getFloat(), getString()... từ đối tượng SharedPreferences. Các phương thức này nhận vào khóa và một giá trị mặc định, giá trị mặc định này có nghĩa là nếu khóa không tồn tại thì tạo khóa mới có giá trị là giá trị mặc định. Ví dụ:

package com.phocode;

import android.content.Context;
import android.content.SharedPreferences;
import android.app.Activity;
import android.os.Bundle;
import android.widget.Toast;

public class MainActivity extends Activity
{
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);  
 
        SharedPreferences pref = getPreferences(Context.MODE_PRIVATE);
        String username = pref.getString("Username", "Default");
        Toast toast = Toast.makeText(getApplicationContext(),username,Toast.LENGTH_LONG);
        toast.show();
    }
}

Trong đoạn code trên, nếu khóa Username đã tồn tại thì sẽ trả về giá trị của khóa đó, ngược lại thì tạo một khóa mới là lưu giá trị mặc định là Default. Lớp Toast là một widget trong Android có tác dụng in một câu thông báo lên màn hình rồi tắt đi.

Untitled

Lưu dữ liệu trong file của hệ điều hành

Nếu dữ liệu cần lưu trữ khá lớn, chẳng hạn như lưu ảnh… thì chúng ta nên dùng cách này. Để lưu dữ liệu lên file thì chúng ta có thể đơn giản là dùng lớp java.io.FileOutputStream của Java là đủ rồi. Hệ thống file của Android cũng không khác gì so với hệ thống file trên các nền tảng khác như Linux, Windows…

Internal và External

Các máy Android có 2 khu vực lưu trữ file là Internal và External, hay chúng ta còn gọi là thẻ nhớ trong và thẻ nhớ ngoài. Trong đó Internal là bộ nhớ mặc định có sẵn của từng máy, còn External là thẻ nhớ ngoài thường là mua thêm rồi gắn vào, nếu không cần thì có thể gỡ ra thay cái khác. Ngoài ra còn có một số máy đặc biệt chia thẻ nhớ trong thành 2 phân vùng là Internal và External, tức là những máy này thực chất chỉ có 1 thẻ nhớ trong nhưng trong máy lúc nào cũng hiện ra 2 thẻ nhớ Internal và External và các thao tác với phân vùng External này giống hệt như với một thẻ nhớ ngoài thật.

Đây là bảng so sánh 2 loại thẻ nhớ:

INTERNAL EXTERNAL
Luôn có sẵn trong máy Có thể có hoặc không vì người dùng có thể gỡ thẻ nhớ ra
Dữ liệu của ứng dụng nào chỉ có thể truy xuất bởi ứng dụng đó Dữ liệu lưu ở đây có thể được đọc bởi bất kì ứng dụng nào tại bất cứ thời điểm nào
Khi ứng dụng bị xóa thì dữ liệu lưu trong này cũng bị xóa Khi ứng dụng bị xóa thì dữ liệu sẽ bị xóa nếu được lưu trong thư mục của ứng dụng

Theo bảng trên thì chúng ta nên sử dụng khu vực Internal nếu dữ liệu cần lưu trữ là loại dữ liệu riêng tư, không thể cho người khác biết. Còn khu vực External dành cho những dữ liệu được chia sẻ cho các ứng dụng khác hoặc với các thiết bị khác.

Mặc định ứng dụng được cài trên bộ nhớ Internal, nhưng chúng ta có thể sử dụng thuộc tính android:installLocation để quy định ứng dụng được cài trên bộ nhớ External, việc này sẽ rất có ích cho các ứng dụng có dung lượng lớn, vì thường bộ nhớ Internal được cài mặc định vào máy có dung lượng khá thấp và không thể thay đổi, trong khi bộ nhớ External có thể tháo ra lắp vào dễ dàng vào thường bộ nhớ External có dung lượng cao hơn Internal nhiều.

Lấy quyền sử dụng bộ nhớ External

Để có thể ghi dữ liệu lên bộ nhớ External thì ứng dụng phải được cấp quyền sử dụng bộ nhớ, chúng ta lấy quyền này bằng cách khai báo trong file AndroidManifest.xml như sau:

<manifest ...>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    ...
</manifest>

WRITE_EXTERNAL_STORAGE là quyền được ghi dữ liệu lên thẻ nhớ, còn READ_EXTERNAL_STORAGE là quyền đọc dữ liệu. Nếu chúng ta khai báo quyền ghi dữ liệu thì ứng dụng sẽ được cấp cả quyền đọc dữ liệu luôn.

Đối với bộ nhớ Internal thì ứng dụng nào cũng có thể đọc ghi được nên không cần xin quyền.

Lưu file lên bộ nhớ Internal

Mặc định trong thư mục cài đặt của mỗi ứng dụng có 2 thư mục tên là filescache, trong đó thư mục files lưu các file như bình thường còn thư mục cache lưu các file dùng tạm, chúng ta chỉ nên lưu các dữ liệu ít quan trọng vào thư mục cache và phải xóa đi khi không còn dùng nữa, nếu không hệ điều hành sẽ tự xóa khi bộ nhớ bị đầy, các loại dữ liệu còn lại thì lưu trong thư mục files.

Chúng ta có thể sử dụng lớp java.io.FileOutputStream của Java là có thể đọc ghi như bình thường. Ví dụ:

package com.phocode;

import android.app.Activity;
import android.os.Bundle;
import java.io.FileOutputStream;
import java.io.IOException;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        String fileName = "save";
        String data = "PhoCode - Open source is the future";
        FileOutputStream file;

        try {
            file = openFileOutput(fileName, Context.MODE_PRIVATE);
            file.write(data.getBytes());
            file.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Mặc định mỗi ứng dụng sẽ được cài đặt vào một thư mục có tên là tên package của ứng dụng. Khi chúng ta ghi file với chế độ Context.MODE_PRIVATE thì chỉ có ứng dụng của chúng ta mới đọc được file đó, các ứng dụng khác không thể đọc được, nếu muốn ứng dụng khác có thể đọc được và ghi được thì chúng ta dùng chế độ là Context.MODE_WORLD_READABLEContext.MODE_WORLD_WRITEABLE, tuy nhiên 2 chế độ này bị xóa khỏi phiên bản SDK 17.

Lưu file lên bộ nhớ External

Thẻ nhớ ngoài có thể có hoặc không có nên trước khi ghi dữ liệu chúng ta nên kiểm tra xem thẻ nhớ ngoài đang có trong máy hay không đã. Chúng ta có thể dùng phương thức Environment.getExternalStorageState() để lấy trạng thái của thẻ nhớ, nếu trạng thái đó là Environment.MEDIA_MOUNTED thì có thẻ nhớ trong máy. Ví dụ:

package com.phocode;

import android.os.Environment;
import android.app.Activity;
import android.os.Bundle;
import android.widget.Toast;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        if(isExternalStorageWritable())
            Toast.makeText(getApplicationContext(), 
                           "External Storage Available", Toast.LENGTH_LONG).show();
        else
            Toast.makeText(getApplicationContext(),
                           "External Storage Not Available", Toast.LENGTH_LONG).show();

        if(isExternalStorageReadable())
            Toast.makeText(getApplicationContext(),
                           "External Storage is Read-Only", Toast.LENGTH_LONG).show();
        else
            Toast.makeText(getApplicationContext(),
                           "External Storage can be read and write", Toast.LENGTH_LONG).show();        
    }

    public boolean isExternalStorageWritable()
    {
        String state = Environment.getExternalStorageState();
        if(Environment.MEDIA_MOUNTED.equals(state))
            return true;
        return false;
    }

    public boolean isExternalStorageReadable()
    {
        String state = Environment.getExternalStorageState();
        if(Environment.MEDIA_MOUNTED.equals(state) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state))
            return true;
        return false;
    }
}

Ngoài ra còn có hằng số Environment.MEDIA_MOUNTED_READ_ONLY cho biết thẻ nhớ này cho phép ghi dữ liệu hay chỉ cho phép đọc không thôi.

Mặc định thẻ nhớ ngoài sẽ chứa những thư mục như Alarm, DCIM, Pictures, Movies… và Android SDK cung cấp cho chúng ta một số hằng số trỏ đến từng thư mục này qua lớp android.os.Enviroment như Environment.DIRECTORY_DCIM, Environment.DIRECTORY_ALARMS... Đây là các thư mục public, tức là chúng được dùng để lưu các dữ liệu dùng chung, ai cũng có thể truy cập được. Trong số các thư mục đó có thư mục tên là Android, thư mục này dùng để chứa dữ liệu của từng ứng dụng riêng biệt, tên thư mục của từng ứng dụng là tên đầy đủ của package trong ứng dụng đó, các thư mục này cũng có thể truy xuất bởi bất kì ai, bất kì ứng dụng nào ở bất kì đâu.

Sự khác nhau giữa các thư mục như DCIM, Alarms, Pictures… với các thư mục bên trong thư mục Android là nếu ứng dụng lưu file vào các thư mục dùng chung thì khi ứng dụng bị xóa thì dữ liệu trong đó không bị xóa, còn nếu ứng dụng lưu dữ liệu trong thư mục của riêng nó (trong thư mục Android) thì khi ứng dụng bị xóa thì dữ liệu trong đó cũng bị xóa theo.

Chúng ta có thể lấy đường dẫn đến các thư mục dùng chung thông qua phương thức Activity.getExternalStoragePublicDirectory(). Phương thức này nhận vào tên thư mục dùng chung và trả về một đối tượng java.io.File. Ví dụ:

File file = new File(Environment.getExternalStoragePublicDirectory(
                     Environment.DIRECTORY_PICTURES));

Nếu muốn lấy đường dẫn thư mục của riêng từng ứng dụng trong thư mục Android thì chúng ta gọi phương thức getExternalFilesDir(null).

Sau khi có đường dẫn thư mục, chúng ta có thể tiến hành đọc ghi file như bình thường với lớp FileOutputStream.

Xem dung lượng còn trống

Nếu bạn biết lượng dữ liệu mà mình sẽ lưu nhưng không biết thẻ nhớ còn trống bao nhiêu, bạn có thể sử dụng 2 phương thức là getFreeSpace()getTotalSpace() của lớp java.io.File để biết được dung lượng còn trống và tổng dung lượng của bộ nhớ là bao nhiêu. Như thế sẽ tránh được lỗi IOException. Những thông tin đó được trả về bởi hệ điều hành nhưng thường thì bạn cũng không thể dùng chính xác dung lượng đó, thường thì khi nào dung lượng còn trống hơn 10% thì hãy sử dụng.

Tất nhiên là bạn cũng không cần thiết phải kiểm tra dung lượng còn trống của ổ đĩa, thay vào đó bạn có thể cho ghi dữ liệu lên thẻ nhớ luôn và chỉ cần bắt lỗi IOException là đủ.

Xóa file

Khi file không còn dùng được nữa bằng phương thức delete() của lớp java.io.File.

file.delete();

Nếu dữ liệu được lưu trên vùng Internal thì chúng ta có thể dùng phương thức deleteFile() của lớp android.app.Activity, phương thức này sẽ nhận vào tên file:

deleteFile(fileName);

Nhắc lại là mặc định khi ứng dụng bị xóa thì hệ điều hành sẽ xóa toàn bộ file trong khu vực Internal và toàn bộ file trong thư mục của riêng ứng dụng ở External, bạn cũng nên tự tay xóa toàn bộ mọi thứ để tránh lãng phí bộ nhớ.