Author Archives: Phở Code

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ớ.

Android – Fragment

Fragment là một lớp trong Android cho phép thao tác với các View, Fragment cũng có các phương thức khởi tạo, phục hồi, hủy… có thế nhận và xử lý sự kiện…v.v như một Activity, nhìn chung thì chúng ta có thể coi Fragment như một Activity con vậy. Nhưng Fragment không tồn tại một mình như Activity mà một Fragment phải được “gắn” với một Activity nào đó, nếu Activity bị hủy thì Fragment cũng bị hủy, nếu Activity được phục hồi thì Fragment cũng được phục hồi.

Bản chất Fragment cũng giống như một View như Button, EditText…v.v  chỉ khác các View ở chỗ là Fragment lại được dùng để chứa các View khác, có vòng đời của riêng nó và có thể xử lý sự kiện.

Nếu bạn đã từng lập trình GUI với Java Swing thì bạn có thể so sánh Fragment giống như JPanel vậy, còn ActivityJFrame.

Thường thì chúng ta dùng Fragment để phân chia công việc của Activity ra cho dễ quản lý. Sử dụng Fragment cho bạn những lợi thế sau:

  • Có thể chia công việc của Activity cho các Fragment để dễ dàng quản lý và sửa đổi code.
  • Có thể dùng một Fragment cho nhiều Activity, do đó khi tạo một Activity chúng ta không cần phải thiết kế lại giao diện mà có thể dùng giao diện có sẵn từ các Fragment đã tạo trước đó.
  • Có khả năng tùy biến giao diện ứng dụng một cách dễ dàng dựa theo nhiều kích thước màn hình khác nhau.

Vòng đời của một Fragment

Cũng giống như Activity, một Fragment cũng có các phương thức trạng thái của riêng nó.

fragment_lifecycle

  • onAttach: phương thức này sẽ gắn Fragment vào một Activity
  • onCreate: sau khi đã được gắn vào Activity, phương thức onCreate() sẽ tạo một đối tượng mới thuộc lớp Fragment.
  • onCreateView: sau đó phương thức onCreateView() sẽ tạo giao diện cho Fragment và gắn giao diện này vào giao diện chính của Activity.
  • onActivityCreated: phương thức này được gọi sau khi phương thức onCreate() của Activity đã chạy xong.
  • onStart: chỉ khi phương thức onStart() được gọi thì Fragment mới có thể thực hiện các tương tác với người dùng.
  • onResume: phương thức này được gọi sau khi phương thức onResume() của Activity được gọi.
  • onPause: phương thức này được gọi khi Fragment bị ẩn, hoặc khi Activity đi vào trạng thái Paused.
  • onStop: phương thức này được gọi khi Fragment bị xóa hoặc bị thay thế bởi Fragment khác, hoặc khi Activity gọi phương thức onStop().
  • onDestroyView: sau khi gọi phương thức onStop() thì phương thức onDestroyView() được gọi để xóa và gỡ giao diện ra khỏi Activity.
  • onDestroy: phương thức này sẽ thực hiện các công việc dọn dẹp cuối cùng.
  • onDetach: phương thức này sẽ gỡ Fragment ra khỏi Activity.

Nhìn chung thì vòng đời của một Fragment cũng giống như của một Activity nhưng có nhiều phương thức hơn.

Ví dụ

Chúng ta tạo một project như bình thường.

Trong file main.xml chúng ta thiết kế như sau:

<?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/button1" 
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:text="Fragment 1" 
        android:onClick="selectFragment"/>
    <Button android:id="@+id/button2" 
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:text="Fragment 2" 
        android:onClick="selectFragment" />
    <fragment android:id="@+id/fragment_place" 
        android:name="com.phocode.FragmentOne" 
        android:layout_width="match_parent" 
        android:layout_height="match_parent" />
</LinearLayout>

Giao diện chính của Activity gồm 2 Button và một thẻ fragment, thẻ này mặc định sẽ dùng lớp FragmentOne để tương tác với người dùng, chúng ta sẽ viết lớp này ở dưới, thẻ fragment này cũng có các thuộc tính width, height... như các View bình thường.

android:onClick="selectFragment"

Hai thẻ Button sẽ có phương thức xử lý sự kiện onClickselectFragment().

Tiếp theo chúng ta định nghĩa 2 lớp Fragment, các lớp Fragment này cũng giống như các Activity là sẽ có 1 file .java để xử lý sự kiện và một file .xml để thiết kế giao diện, tất nhiên nếu muốn bạn cũng có thể làm tất cả trong file .java luôn cũng được.

Chúng ta tạo 2 file .java như sau:

package com.phocode;

import android.app.Fragment;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.view.LayoutInflater;

public class FragmentOne extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, 
                             Bundle savedInstanceState)
    {
        return inflater.inflate(R.layout.fragment_one, container, false);
    }
}
package com.phocode;

import android.app.Fragment;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class FragmentTwo extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, 
                             Bundle savedInstanceState)
    {
        return inflater.inflate(R.layout.fragment_two, container, false);
    }
}

Hai lớp này sẽ kế thừa từ lớp android.app.Fragment.

public View onCreateView(LayoutInflater inflater, ViewGroup container, 
                             Bundle savedInstanceState)
{
    ...
}

Đối với Activity thì các công việc xây dựng giao diện được thực hiện trong phương thức onCreate(), còn đối với Fragment thì là phương thức onCreateView(). Phương thức này nhận vào một đối tượng LayoutInflater, ViewGroup và một đối tượng Bundle.

return inflater.inflate(R.layout.fragment_two, container, false);

Phương thức LayoutInflater.inflate() sẽ thiết kế giao diện cho lớp Fragment, phương thức này nhận vào tên file layout, tham số thứ 2 là đối tượng chứa Fragment này, trong ví dụ này là lớp LinearLayout, tham số thứ 3 cho biết Fragment có được gắn vào LinearLayout đó hay không, ở đây hai tham số sau cũng không quan trọng lắm vì trước sau gì chúng ta cũng gắn Fragment vào LinearLayout.

Sau khi đã có 2 file .java của 2 Fragment thì chúng ta tạo 2 file layout cho 2 Fragment này như sau:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
        android:layout_width="match_parent" 
        android:layout_height="match_parent" 
        android:orientation="vertical" 
        android:background="#00ffff">
 
    <TextView android:id="@+id/textView1" 
        android:layout_width="match_parent" 
        android:layout_height="match_parent" 
        android:layout_weight="1" 
        android:text="Fragment number 1"
        android:textStyle="bold" />
</LinearLayout>
<?xml version="1.0" encoding="utf-8" ?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
        android:layout_width="match_parent" 
        android:layout_height="match_parent" 
        android:orientation="vertical" 
        android:background="#ffff00">
 
    <TextView android:id="@+id/textView2" 
        android:layout_width="match_parent"
        android:layout_height="match_parent" 
        android:text="Fragment number 2"/>
</LinearLayout>

Các file layout này khá đơn giản, chỉ chứa một TextView.

android:background="#ffff00"

Thuộc tính android:background sẽ quy định màu nền cho Fragment.

Cuối cùng là file MainActivity.java

package com.phocode;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;

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);
    }
 
    public void selectFragment(View view)
    {
        Fragment fr;
 
        if(view == findViewById(R.id.button2))     
            fr = new FragmentTwo();     
        else    
            fr = new FragmentOne();        
 
        FragmentManager fm = getFragmentManager();
        FragmentTransaction ft = fm.beginTransaction();
        ft.replace(R.id.fragment_place, fr);
        ft.commit();
    }
}

Chúng ta định nghĩa phương thức selectFragment() để xử lý sự kiện onClick() của Button.

Fragment fr;
 
if(view == findViewById(R.id.button2))     
    fr = new FragmentTwo();     
else    
    fr = new FragmentOne();

Ở đây chúng ta chỉ đơn giản là nếu ấn button Fragment nào thì sẽ cho hiện ra file layout của Fragment đó.

FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.fragment_place, fr);
ft.commit();

Để có thể thực hiện các thao tác với Fragment thì chúng ta sử dụng đến lớp FragmentManager, lớp này sẽ tạo một đối tượng FragmentTransaction, đối tượng này cung cấp các phương thức để tạo, xóa, thay thế… các đối tượng Fragment. Phương thức replace() sẽ thay thế một Fragment bằng Fragment khác. Ngoài còn có một số phương thức khác như add() có tác dụng gắn thêm một Fragment mới vào giao diện.

Capture

Click vào nút Fragment 2 để hiển thị giao diện khác.

Capture

Android – Thao tác với Activity

Trong bài này chúng ta sẽ tìm hiểu về vòng đời của một Activity, thường thì những kiến thức dưới đây cũng không được áp dụng nhiều nhưng chúng ta cũng nên tìm hiểu sơ qua.

Không giống như các ngôn ngữ như C++, Java… các ứng dụng bắt đầu chạy từ một hàm có tên là main(), khi người dùng chạy ứng dụng Android thì ứng dụng sẽ tạo một đối tượng Activity để tương tác với người dùng, và đối tượng Activity này còn có các trạng thái khác nhau trong quá trình chạy nữa. Ví dụ như khi mới bắt đầu khởi động ứng dụng thì Activity sẽ thay thế hệ điều hành làm công việc nhận và xử lý các thông tin được gửi tới từ người dùng, tại đây hệ điều hành sẽ gọi các phương thức cần thiết để khởi tạo các thành phần chủ chốt. Khi người dùng thực hiện một hành động mà có tạo ra một Activity mới hay chuyển sang dùng một ứng dụng khác, hệ điều hành lại gọi các phương thức cần thiết khác để khởi tạo còn Activity hiện tại sẽ được chuyển sang trạng thái background, tức là Activity này không còn hiện lên trên màn hình nữa nhưng nó không bị xóa đi mà vẫn còn nằm đâu đó bên dưới hệ điều hành.

Cũng vì Activity có những trạng thái khác biệt như thế nên chúng ta cũng nên tùy biến ứng dụng theo Activity nữa, ví dụ như khi chúng ta xây dựng một ứng dụng xem video, khi người dùng thoát khỏi activity thì chúng ta nên cho tạm dừng các công việc decode, hiển thị frame, ngắt kết nối mạng…v.v rồi khi nào người dùng quay trở lại thì chúng ta mở lại video tại vị trí đã dừng.

Các phương thức Activity

Trong quá trình chạy của một Activity, hệ điều hành sẽ gọi một số phương thức theo một thứ tự nhất định, và mỗi phương thức này sẽ chuyển ứng dụng sang các trạng thái khác nhau. Chúng ta có thể xem các phương thức và trạng thái tương ứng trong hình dưới, trong đó ứng dụng bắt đầu từ phương thức onCreate() và quá trình chạy di chuyển dần lên phía trên phương thức onResume(), quá trình hủy đi dần dần xuống dưới phương thức onDestroy().

basic-lifecycle-paused

Ngoài ra Activity còn có thể đi từ trạng thái Paused (tạm dừng) và Stopped (dừng) quay lại Resumed (phục hồi).

Thường thì các phương thức này được xử lý tự động bởi hệ điều hành, nhưng cũng có một vài trường hợp chúng ta phải override lại giống như với phương thức onCreate(). Việc override các phương thức đảm bảo ứng dụng của chúng ta chạy tốt trong mọi tình huống như không bị crash khi có cuộc gọi đến hoặc người dùng chuyển sang một ứng dụng khác, hay không làm tiêu tốn tài nguyên nhiều khi người dùng không tương tác với ứng dụng, không bị ảnh hưởng bởi thao tác xoay thiết bị…

Trong hình trên chúng ta đã thấy một Activity có nhiều trạng thái khác nhau, nhưng thực ra chỉ có 3 trạng thái là có thể tồn tại trong khoảng thời gian dài là Resumed, PausedStopped. Trạng thái Resumed là trạng thái mà Activity đó đang thực sự hoạt động và tương tác với người dùng. Trạng thái Paused xảy ra khi có một Activity khác được sinh ra và thực hiện việc thao tác với người dùng thay cho Activity cũ, lúc này Activity cũ sẽ không nhận tương tác với người dùng và xử lý chúng nữa. Trạng thái Stopped là trạng thái khi Activity không còn hiện hình nữa, lúc này Activity không bị xóa nhưng cũng không nhận tương tác với người dùng.

Các trạng thái còn lại là Created, StartedDestroyed chỉ xảy ra trong một khoảng thời gian nhất định và hầu như không còn xảy ra sau đó nữa.

Khai báo main Activity

Một ứng dụng có thể có nhiều Activity, nhưng phải có một Activity chính được khởi tạo khi người dùng chạy chương trình, hệ điều hành sẽ gọi phương thức onCreate() của Activity này.

Chúng ta khai báo Activity chính trong file AndroidManifest.xml nằm ở thư mục gốc của project. Ở đây chúng ta khai báo thẻ intent-filter với thẻ actioncategory như sau:

<activity android:name=".MainActivity" android:label="@string/app_name">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

Nếu chúng ta không khai báo main Activity nào thì ứng dụng sẽ không hiện ra trên màn hình của thiết bị.

Tạo Activity

Hầu hết các ứng dụng đều có nhiều hơn một Activity cho phép người dùng thực hiện nhiều công việc khác nhau. Khi thiết kế một Activity thì chúng ta phải override phương thức onCreate(), phương thức này sẽ làm các công việc mang tính khởi tạo như khởi tạo giao diện người dùng, khai báo biến, hằng số…v.v Ví dụ:

TextView textView;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main_activity);
    textView = (TextView) findViewById(R.id.textView);
}

Sau khi hệ điều hành hoàn tất các công việc trong phương thức onCreate() thì sẽ gọi đến 2 phương thức onStart()onResume(). Tham số của phương thức onCreate() là một đối tượng Bundle, chúng ta sẽ tìm hiểu về đối tượng này sau.

Hủy Activity

Có phương thức khởi tạo Activity thì cũng sẽ có phương thức hủy Activity và đó là phương thức onDestroy(), phương thức này được gọi bởi hệ điều hành khi ứng dụng thông báo cho hệ điều hành biết là nó đã hoàn thành công việc và chuẩn bị xóa khỏi bộ nhớ.

Hầu như chúng ta không phải override lại phương thức này vì các công việc như dọn dẹp bộ nhớ, xóa biến…v.v đều được làm tự động bởi hệ điều hành. Tuy nhiên chúng ta nên override lại phương thức này khi ứng dụng của chúng ta có sử dụng đến các luồng (thread) chạy ngầm vì chúng sẽ ngốn rất nhiều tài nguyên kể cả khi chúng ta đã thoát ứng dụng chính. Ví dụ:

@Override
public void onDestroy() {
    super.onDestroy();  
    android.os.Debug.stopMethodTracing();
}

Tạm dừng Activity

Đôi khi một màn hình giao diện của một Activity sẽ bị che khuất một phần bởi các thành phần khác như hộp thoại thông báo… lúc này Activity sẽ chuyển sang trạng thái Paused được gọi bởi phương thức onPause(), một phần của Activity vẫn hiện ra trên màn hình nhưng không xử lý tương tác với người dùng. Khi người dùng quay trở lại Activity thì hệ thống sẽ gọi phương thức onResume()Activity sẽ quay lại trạng thái Resumed. 

Thông thường khi Activity ở trạng thái Paused tức là sau đó người dùng sẽ thoát luôn chứ không phải là quay lại nữa, do đó chúng ta nên làm một số công việc như xóa tài nguyên, lưu dữ liệu đang làm việc dang dở…v.v ở phương thức này. Ví dụ:

@Override
public void onPause() {
    super.onPause();

    if (mCamera != null) {
        mCamera.release();
        mCamera = null;
    }
}

Những công việc thực hiện trong phương thức onPause() nên được đơn giản hóa và không phức tạp để tăng tốc cho ứng dụng.

Khôi phục Activity

Activity sẽ được khôi phục từ trạng thái Paused sang Resumed từ phương thức onResume().

Hệ điều hành sẽ gọi phương thức onResume() mỗi khi Activity hiện lên trên màn hình, kể cả lần khởi tạo đầu tiên. Chúng ta override phương thức này cho mỗi lần khởi tạo ứng dụng. Ví dụ:

@Override
public void onResume() {
    super.onResume(); 

    if (mCamera == null) {
        initializeCamera(); 
    }
}

Dừng Activity

Activity sẽ chuyển sang trạng thái Stopped (dừng) khi phương thức onStop() được gọi, lúc này Activity không còn hiện hữu trên màn hình, ở trong phương thức này chúng ta sẽ thực hiện giải phóng tài nguyên giống như trong phương thức onPause(). Trong một số trường hợp, hệ điều hành có thể hủy ứng dụng mà không gọi tới phương thức onStop(), do đó chúng ta cũng nên override lại phương thức này đề phòng hao tổn bộ nhớ.

Phương thức onStop() được gọi sau phương thức onPause() nhưng hầu hết chúng ta sẽ thực hiện các công việc dọn dẹp và lưu trữ nhiều hơn trong phương thức onStop() như ghi dữ liệu đang làm việc vào cơ sở dữ liệu…v.v. Ví dụ:

@Override
protected void onStop() {
    super.onStop();

    ContentValues values = new ContentValues();
    values.put(NotePad.Notes.COLUMN_NAME_NOTE, getCurrentNoteText());
    values.put(NotePad.Notes.COLUMN_NAME_TITLE, getCurrentNoteTitle());

   getContentResolver().update(
        mUri, 
        values, 
        null, 
        null
   );
}

Khi Activity đi vào trạng thái Stopped, đối tượng Activity vẫn không bị xóa mà còn nằm trong bộ nhớ và có thể được phục hồi nên chúng ta có thể không cần phải khởi tạo lại những thành phần không bị xóa. Hệ điều hành thậm chí còn lưu lại trạng thái của từng View, giả sử như người dùng nhập text vào một EditText, đoạn text đó sẽ được lưu lại và khi phục hồi Activity thì đoạn text đó cũng sẽ được phục hồi.

Khởi động/khởi động lại Activity

Khi Activity được tạo ra lần đầu tiên, hệ điều hành sẽ gọi đến phương thức onCreate→onStart()→onResume(), khi Activity được khôi phục từ trạng thái Stopped, hệ điều hành sẽ gọi đến phương thức onRestart()→onStart()→onResume().

Phương thức onRestart() được gọi khi Activity được khôi phục, do đó chúng ta có thể chỉ cần khởi tạo lại những thành phần mà không bị xóa. Nhưng thông thường thì trong phương thức onStop() chúng ta sẽ xóa gần như toàn bộ mọi thứ nên phương thức onRestart()onStart() sẽ phải khởi tạo lại tất cả mọi thứ, do đó việc override phương thức onRestart() cũng không cần thiết lắm.

Ví dụ:

@Override
protected void onStart() {
    super.onStart();
 
    LocationManager locationManager = 
    (LocationManager) getSystemService(Context.LOCATION_SERVICE);
    boolean gpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
 
    if (!gpsEnabled) {
        ... 
    }
}

@Override
protected void onRestart() {
    super.onRestart();
 
}

Sau khi hệ điều hành đã hủy Activity, phương thức onDestroy() sẽ được gọi nhưng thường thì chúng ta cũng không làm việc với phương thức này vì hầu như toàn bộ công việc dọn dẹp sẽ được thực hiện trong phương thức onStop(). Tuy nhiên nếu muốn bạn có thể override luôn và phương thức onDestroy() là phương thức cuối cùng để chúng ta thực hiện giải phóng tài nguyên, nên bạn cũng cần chắc chắn là đã xóa tất cả mọi thứ đề phóng trường hợp thất thoát tài nguyên.

Android – Tạo Activity

Trong phần trước chúng ta đã tạo một Activity, trong phần này chúng ta sẽ tiếp tục đoạn code ở phần trước và tạo một Activity mới khi click vào nút Send. 

Activity là một thành phần xử lý các hành động, thường thì Activity sẽ làm các công việc khởi tạo giao diện và xử lý sự kiện trên giao diện đó, thông thường chúng ta sẽ thiết kế từng giao diện ứng với từng Activity nên một Activity cũng hay được gọi là một màn hình (screen).

Xử lý sự kiện nút Send

Chúng ta sửa lại file layout chính ở bài trước.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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" 
    android:orientation="horizontal" >
    <EditText android:id="@+id/edit_message" 
        android:layout_width="0dp" 
        android:layout_height="wrap_content" 
        android:layout_weight="1" 
        android:hint="@string/edit_message"/>
    <Button android:layout_height="wrap_content" 
        android:layout_width="wrap_content" 
        android:text="@string/button_send" 
        android:onClick="sendMessage"/>
</LinearLayout>

Chúng ta khai báo thêm thuộc tính android:onClick cho thẻ Button, sendMessage là tên phương thức xử lý khi người dùng click vào nút Button.

Trong file MainActivity.java chúng ta sửa lại như sau:

package com.phocode.myfirstapp;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;

public class MainActivity extends Activity {

    public final static String EXTRA_MESSAGE = "com.phocode.myfirstapp.MESSAGE";

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

    public void sendMessage(View view)
    {
        Intent intent = new Intent(this, DisplayMessageActivity.class);
        EditText editText = (EditText)findViewById(R.id.edit_message);
        String message = editText.getText().toString();
        intent.putExtra(EXTRA_MESSAGE, message);
        startActivity(intent);
    }
}

Chúng ta khai báo phương thức sendMessage tương ứng với giá trị của thuộc tính android:onClick trong file XML.

public final static String EXTRA_MESSAGE = "com.phocode.myfirstapp.MESSAGE";

Chúng ta khai báo một hằng số có tên là EXTRA_MESSAGE để dùng cho việc gửi dữ liệu đi tới Activity khác (sẽ trình bày ở dưới). Ở đây giá trị của hằng số này chúng ta dùng chính tên package cho khỏi trùng lắp và dễ nhớ, tất nhiên bạn có thể dùng giá trị khác.

public void sendMessage(View view)
{
    ...
}

Phương thức sendMessage nhận vào một đối tượng View, đây chính là đối tượng Button được click, tức là khi chúng ta chạy ứng dụng và click vào nút “Send” thì ứng dụng sẽ gọi phương thức sendMessage và gửi vào đó biến tham chiếu đến đối tượng Button này.

Lưu ý là các phương thức xử lý sự kiện như phương thức sendMessage trên phải được khai báo public, có kiểu trả về là void và tham số duy nhất là một đối tượng View.

Intent intent = new Intent(this, DisplayMessageActivity.class);

Intent là một đối tượng tạo cầu nối giữa các thành phần khác nhau trong khi ứng dụng đang chạy, nói chung là bạn chỉ cần hiểu Intent giống như một máy nhắn tin giúp trao đổi thông tin giữa các thành phần khác nhau vậy, Intent có rất nhiều công dụng nhưng thường thì người ta dùng Intent để chạy một Activity mới.

Tham số của phương thức khởi tạo Intent là một đối tượng lớp Context, ở đây chúng ta truyền this là vì lớp Activity kế thừa từ lớp Context, tham số thứ 2 là tên lớp Activity mà nó sẽ truyền tới, ở đây là lớp DisplayMessageActivity, chúng ta sẽ định nghĩa lớp này sau.

EditText editText = (EditText)findViewById(R.id.edit_message);
String message = editText.getText().toString();

Tiếp theo chúng ta lấy đối tượng EditText đã khai báo trong file main.xml bằng phương thức findViewById(), ở đây chúng ta truyền vào tên thuộc tính id đã khai báo, rồi ép kiểu về lớp EditText bởi vì phương thức này trả về một đối tượng lớp View. Sau đó chúng ta lấy đoạn chuỗi được gõ trong EditText này bằng phương thức getText(), phương thức này trả về một đối tượng Editable nên chúng ta dùng phương thức toString() để lấy giá trị ra kiểu String.

intent.putExtra(EXTRA_MESSAGE, message);

Đối tượng Intent có thể mang theo dữ liệu khi gọi một Activity mới, để truyền dữ liệu đi thì chúng ta dùng phương thức putExtra(), phương thức này nhận vào dữ liệu dạng từ điển, tức là một cặp khóa-giá tr, ở đây chúng ta dùng khóa là hằng số EXTRA_MESSAGE mà chúng ta đã khai báo ở đầu lớp, giá trị là dữ liệu trong biến message.

startActivity(intent);

Cuối cùng để chạy một Activity thì chúng ta gọi phương thức startActivity() và truyền vào một đối tượng Intent.

Tạo Activity mới

Như đã trình bày ở đầu bài, một Activity thông thường sẽ làm công việc thiết kế giao diện của một màn hình và xử lý các thao tác xảy ra trên màn hình đó. Trong một ứng dụng Android thì để thiết kế giao diện của một Activity chúng ta có 2 cách là code “chay” bằng Java bên trong file .java, 2 là thiết kế riêng trong một file XML. Ở đây chúng ta sẽ thiết kế trong file XML.

Chúng ta tạo một file XML có tên là activity_display_message.xml bên trong thư mục res/layout:

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

Ở đây chúng ta khai báo RelativeLayout với các thuộc tính width, heightid. Chi tiết về các layout sẽ được trình bày ở bài khác.

Tiếp theo chúng ta định nghĩa lớp Activity mới.

package com.phocode.myfirstapp;

import android.content.Intent;
import android.app.Activity;
import android.os.Bundle;
import android.widget.RelativeLayout;
import android.widget.TextView;

public class DisplayMessageActivity extends Activity {

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

        Intent intent = getIntent();
        String message = intent.getStringExtra(MainActivity.EXTRA_MESSAGE);
        TextView textView = new TextView(this);
        textView.setTextSize(40);
        textView.setText(message);

        RelativeLayout layout = (RelativeLayout) findViewById(R.id.content);
        layout.addView(textView);
    }
}

Lớp DisplayMessageActivity sẽ kế thừa từ lớp Activity.

setContentView(R.layout.activity_display_message);

Khi biên dịch, các file layout sẽ được gộp lại thành một file có tên là R.java trong thư mục gen/, file này sẽ lưu các ID cho từng layout. Nhờ đó chúng ta có thể tham chiếu đến các layout ngay bên trong code Java.

Intent intent = getIntent();

Bất cứ Activity nào cũng được gọi từ một đối tượng Intent, chúng ta có thể tham chiếu đến đối tượng này thông qua phương thức getIntent().

String message = intent.getStringExtra(MainActivity.EXTRA_MESSAGE);

Đối tượng Intent của chúng ta có mang theo dữ liệu, chúng ta có thể lấy dữ liệu đó qua phương thức getStringExtra(), phương thức này nhận vào khóa và trả về giá trị tương ứng.

TextView textView = new TextView(this);
textView.setTextSize(40);
textView.setText(message);

Chúng ta gán giá trị được gửi tới vào một đối tượng TextView, ở đây chúng ta khai báo trong code Java chứ không khai báo trong file layout. Phương thức setTextSize() quy định kích thước chữ, phương thức setText() sẽ quy định nội dung trong TextView đó.

RelativeLayout layout =(RelativeLayout) findViewById(R.id.content);
layout.addView(textView);

Cuối cùng chúng ta tham chiếu đến đối tượng RelativeLayout thông qua phương thức findViewById() và chèn TextView vào layout này bằng phương thức addView().

Sau khi đã định nghĩa xong lớp Activity thì chúng ta phải khai báo Activity đó trong file AndroidManifest.xml.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.phocode" android:versionCode="1" android:versionName="1.0">
    <application android:label="@string/app_name" android:icon="@drawable/ic_launcher">
    <activity android:name="MainActivity" android:label="@string/app_name">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
    <activity android:name="DisplayMessageActivity"></activity>
    </application>
</manifest>

Chúng ta chỉ cần khai báo thêm một thẻ activity với thuộc tính name là tên lớp Activity mà chúng ta sẽ định nghĩa.

Capture

Biên dịch và chạy ứng dụng, bây giờ khi bấm vào nút “Send” thì ứng dụng sẽ mở một màn hình thứ hai có một chuỗi text lấy từ EditText của màn hình trước.

Capture

 

Android – Thiết kế giao diện người dùng

Giao diện người dùng của một ứng dụng Android được xây dựng bằng các đối tượng ViewViewGroup. View là các đối tượng như button (nút bấm), text field (ô gõ văn bản)… ViewGroup là các khung dùng để chứa các đối tượng View khác, ViewGroup quy định kích thước cũng như vị trí của các View con nằm trong nó, chẳng hạn như sắp xếp các View theo chiều dọc/ngang, hoặc theo dạng bảng…

Android định nghĩa một tập các thẻ XML tương ứng với các lớp ViewViewGroup để chúng ta có thể thiết kế giao diện theo cú pháp XML một cách dễ dàng.

Từ lớp gốc ViewGroup, các lớp con kế thừa được gọi với tên là Layout. Trong phần này chúng ta sẽ làm việc với LinearLayout.

Tạo LinearLayout

Chúng ta dùng thẻ LinearLayout trong file main.xml như sau:

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

Lớp LinearLayout kế thừa từ lớp ViewGroup, lớp này sẽ sắp xếp các View con theo chiều ngang hoặc chiều dọc. Các phần tử sẽ được sắp xếp theo thứ tự mà chúng được khai báo trong file XML này.

android:orientation="horizontal"

Thuộc tính android:orientation sẽ quy định là sắp xếp theo chiều ngang hay dọc, ở đây horizontal là ngang, còn vertical sẽ là dọc.

android:layout_width="match_parent"
android:layout_height="match_parent"

Hai thuộc tính android:layout_widthandroid:layout_height là bắt buộc phải có, 2 thuộc tính này sẽ quy định kích thước cho đối tượng View. Giá trị "match_parent" tức là kích thước của View con sẽ bằng kích thước của View cha, nhưng vì LinearLayout là lớp View gốc, tức là lớp này không có View cha nào cả nên ở đây lớp LinearLayout này sẽ có kích thước bằng với kích thước màn hình.

Thêm TextField

<?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">
    <EditText android:id="@+id/edit_message" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:hint="@string/edit_message" />
</LinearLayout>

Chúng ta thêm thẻ EditText vào bên trong thẻ LinearLayout.

android:id="@+id/edit_message"

Thuộc tính android:id là một cái tên riêng dành cho đối tượng View mà chúng ta đã khai báo, chúng ta có thể tham chiếu tới id này giống như tham chiếu tới biến trong các file .java. Dấu @ có nghĩa là giá trị của biến này được định nghĩa trong các đối tượng resource (tiếng Anh là tài nguyên) chứ không khai báo trực tiếp ở đây, theo sau dấu @ là loại resource, ở đây là id, tiếp theo là dấu / và tên resouce edit_message.

Dấu + phía trước id có nghĩa là tại đây chúng ta định nghĩa luôn id chứ mặc định Android không biết id là cái gì cả. Khi chúng ta dịch project thì SDK sẽ nhìn cái id đó và định nghĩa một lớp có tên là ID bên trong file gen/R.java, các resource có cùng loại id sau này sẽ tự động được thêm vào ở lớp ID này để gán vào các thẻ View của chúng ta. Khi chúng ta định nghĩa một loại resource mà có dấu + thì các resource sau này có cùng loại không cần phải có dấu + nữa.

android:layout_width="wrap_content"
android:layout_height="wrap_content"

Hai thuộc tính layout_widthlayout_height cũng giống với LinearLayout, nhưng ở đây chúng ta sử dụng giá trị wrap_content, giá trị này quy định kích thước của View là vừa đủ để bọc lấy nội dung bên trong nó, chẳng hạn như EditText có 3 kí tự thì kích thước sẽ tự động co dãn để vừa khớp với 3 kí tự đó.

android:hint="@string/edit_message"

Thuộc tính android:hint sẽ hiển thị một chuỗi mặc định nếu chúng ta không gõ gì vào text field. Ở đây giá trị của thuộc tính này tham chiếu tới biến edit_message trong loại resource là string, đây là loại resource có sẵn trong file strings.xml của project nên chúng ta không cần phải thêm dấu + vào làm gì, nhưng chúng ta phải khai báo biến edit_message trong file này, chúng ta sẽ khai báo ở dưới.

Mặc dù cả 2 thuộc tính android:idandroid:hint đều tham chiếu tới resource có cùng tên là edit_message nhưng đây là 2 resource khác nhau vì chúng nằm trong 2 loại resource khác nhau là idstring.

Khai báo tài nguyên string

Mặc định khi tạo project Android thì project này có một file resource chứa loại stringres/values/strings.xml.

<?xml version="1.0" encoding="utf-8"?>
<resources>
     <string name="app_name">MainActivity</string>
     <string name="edit_message">Enter a message</string>
     <string name="button_send">Send</string>
     <string name="action_settings">Settings</string>
</resources>

Chúng ta sẽ khai báo thêm một số resource ở đây.

<string name="edit_message">Enter a message</string>

Dòng code trên là cách khai báo một resource có kiểu là string với tên là edit_message, giá trị là Enter a message.

Thường thì khi thiết kế giao diện mà có những giá trị text thì chúng ta nên khai báo bằng biến trong file resource chứ không nên khai báo trực tiếp, tức là thay vì khai báo android:hint="Enter a message" thì chúng ta nên khai báo android:hint="@string/edit_message" hơn, rồi trong file strings.xml chúng ta khai báo thẻ string với nameedit_message. Điều này giúp chúng ta dễ dàng sửa đổi cũng như cập nhật lại sau này.

Thêm 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">
    <EditText android:id="@+id/edit_message" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:hint="@string/edit_message" />
    <Button android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:text="@string/button_send"/>
</LinearLayout>

Chúng ta thêm một đối tượng ViewButton.

Các thuộc tính của thẻ Button cũng giống các thuộc tính của 2 thẻ kia, chỉ khác ở chỗ là ở đây chúng ta không khai báo id, thuộc tính id là một thuộc tính không bắt buộc.

Hiện tại giao diện của ứng dụng mà chúng ta đã thiết kế sẽ như hình bên dưới.

Capture

Nhìn thì có vẻ không có vấn đề gì, nhưng nếu chúng ta nhập một chuỗi có chiều dài lớn hơn kích thước màn hình thì text field sẽ tự dãn ra và đẩy button biến mất.

Để giải quyết vấn đề này chúng ta có thể dùng đến thuộc tính android:layout_weight, thuộc tính này quy định kích thước của các View theo tỉ lệ.

Ví dụ nếu chúng ta khai báo EditTextweight là 2 và Buttonweight là 1, như thế màn hình sẽ được chia làm 1 + 2 = 3 phần, trong đó EditText sẽ chiếm 2 phần và Button sẽ chiếm 1 phần. Khi chạy ứng dụng thì các View sẽ tự động dãn ra để lấp vừa 3 phần đó, nhưng dãn vừa đủ để EditText vẫn chiếm 2/3 kích thước còn Button chiếm 1/3 kích thước.

Chúng ta sửa lại main.xml như sau:

<?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">
    <EditText android:id="@+id/edit_message" 
        android:layout_width="0dp" 
        android:layout_height="wrap_content" 
        android:layout_weight="1" 
        android:hint="@string/edit_message" />
    <Button android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:text="@string/button_send"/>
</LinearLayout>

Mặc định thuộc tính weight của các View là 0, nếu chúng ta khai báo weight cho View nào lớn hơn 0 thì view đó sẽ tự động dãn kích thước ra để lấp đầy khoảng trống còn thừa. Ở đây chúng ta khai báo weight cho EditText là 1.

android:layout_width="0dp"

Ngoài ra ở đây chúng ta cũng thiết lập lại thuộc tính android:layout_width của EditText là 0 bởi vì mặc định wrap_content sẽ yêu cầu ứng dụng phải tính toán kích thước của View dựa trên số lượng kí tự trong đó, nhưng sau đó thuộc tính weight lại yêu cầu ứng dụng dãn EditText ra để vừa khít với phần kích thước còn thừa, như thế việc tính toán kích thước của wrap_content là chẳng để làm gì cả, do đó chúng ta thiết lập “đại” sẵn một con số cụ thể nào đó luôn để ứng dụng khỏi mất công tính nữa, như thế sẽ tăng tốc độ thực thi của ứng dụng lên rất nhiều.

Capture

Chúng ta biên dịch và chạy lại ứng dụng với giao diện mới như hình trên.

Android – Tạo project Android

Một project Android sẽ chứa những file cần thiết dành cho một ứng dụng Android.

Tạo project trong Android Studio

Tạo project trong Android Studio rất đơn giản, chỉ cần vào File→New Project. Sau đó chúng ta thiết lập các thông số cho project:

  • Application Name: tên project, mình đặt là My First App
  • Company domain: đây là tên sẽ được chèn vào trước tên package
  • Package name: đây là tên package cho project, mình đặt com.phocode
  • Project location: đường dẫn đến thư mục chứa project
  • Select the form factors your app will run on: chọn Phone and Tablet
  • Minimum SDK: chọn API 8 nếu bạn đã cài phiên bản Android 2.2 (Froyo) trong SDK Manager, không thì bạn chọn API thấp nhất hiện có trong SDK Manager. Đây là phiên bản Android thấp nhất mà ứng dụng của bạn hỗ trợ.
  • Add an Activity to Mobile: chọn Empty Activity
  • Activity NameLayout Name: đặt tên file Activity là file layout, bạn có thể để mặc định

Cuối cùng nhấn Finish.

Tạo project từ dòng lệnh

Nếu bạn không cài Android Studio mà chỉ cài bộ SDK thôi thì bạn có thể tạo project từ dòng lệnh như sau:

C:\Project\Android\MyFirstApp>android create project --target android-8 --name MyFirstApp 
--path . --activity MainActivity --package com.phocode
Created directory C:\Project\Android\src\com\phocode
Added file C:\Project\Android\src\com\phocode\MainActivity.java
Created directory C:\Project\Android\res
Created directory C:\Project\Android\bin
Created directory C:\Project\Android\libs
Created directory C:\Project\Android\res\values
Added file C:\Project\Android\res\values\strings.xml
Created directory C:\Project\Android\res\layout
Added file C:\Project\Android\res\layout\main.xml
Created directory C:\Project\Android\res\drawable-hdpi
Created directory C:\Project\Android\res\drawable-mdpi
Created directory C:\Project\Android\res\drawable-ldpi
Added file C:\Project\Android\AndroidManifest.xml
Added file C:\Project\Android\build.xml
Added file C:\Project\Android\proguard-project.txt

Lệnh android create project sẽ tạo một project Android tại thư mục hiện hành trong Command Prompt. Tham số target là phiên bản SDK thấp nhất, name là tên project, path là đường dẫn đến thư mục tạo project, ở đây dấu chấm có nghĩa là đường dẫn chỉ đến thư mục hiện tại, package là tên package, activity là tên file Activity được tạo ra.

Project được tạo ra cho dù là từ Android Studio hay từ dòng lệnh cũng đều có các thư mục và file cơ bản sau đây:

bin/
libs/
res/
src/
AndroidManifest.xml  
ant.properties
build.xml
local.properties
proguard-project.txt
project.properties    

Trong đó file AndroidManifest.xml sẽ mô tả các tính chất của chương trình. Các file .java sẽ nằm trong thư mục src/. Các file tài nguyên như file XML, file ảnh… sẽ nằm trong thư mục res/. Các file apk sau khi được tạo ra sẽ nằm trong thư mục bin/. Thư mục libs/ chứa các thư viện hỗ trợ. Hai file ant.propertiesbuild.xml là các file Ant dùng cho việc biên dịch project. Cuối cùng là file local.propertiesproject.properties lưu các đặc tính của project.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.phocode" android:versionCode="1" android:versionName="1.0">
    <application android:label="@string/app_name" android:icon="@drawable/ic_launcher">
        <activity android:name="MainActivity" android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

File AndroidManifest.xml lưu trữ các thông tin về project mà chúng ta đã tạo, như tên package, tên file Activity… Hai chuỗi @string/app_name@drawable/ic_launcher là các giá trị tài nguyên được lưu trong các file tài nguyên trong thư mục res/, chúng ta sẽ tìm hiểu sau. Thẻ <intent-filter> nằm bên trong thẻ <activity> khai báo các công việc mà Activity có thể thực hiện, trong đó có một thẻ <action> và một thẻ <category>, cả 2 thẻ này cho biết Activity này là Activity chính được chạy khi ứng dụng chạy.

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

File strings.xml nằm bên trong thư mục res/values định nghĩa các giá trị chuỗi được sử dụng trong ứng dụng. Có thể hiểu đây giống như các biến toàn cục/hằng số được định nghĩa trước vậy. Thẻ <string> sẽ định nghĩa một biến với tên nằm trong thuộc tính name. Mặc định mỗi project Android được tạo ra sẽ có một biến tên là app_name có giá trị là tên Activity mà chúng ta tạo ra.

<?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="Hello World, MainActivity" />
</LinearLayout>

File main.xml nằm trong thư mục res/layout. File này định nghĩa giao diện cho một Activity. Khi chạy ứng dụng thì giao diện này sẽ được gọi từ phương thức onCreate().

package com.phocode;

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);
    }
}

File MainActivity.java sẽ tạo một Activity trong phương thức onCreate() như đã nói ở trên. Mặc định phương thức này sẽ thiết lập giao diện ứng dụng là file main.xml với phương thức setContentView().

Chạy chương trình từ Android Studio

Để biên dịch và chạy ứng dụng android thì chúng ta bật máy ảo lên hoặc kết nối máy thật vào máy tính thông qua cổng USB rồi bấm nút Run có biểu tượng hình tam giác màu xanh lá cây. Android Studio sẽ hiển thị một hộp thoại cho chúng ta chọn danh sách các thiết bị có thể chạy.

Lưu ý là nếu bạn chạy trên thiết bị thật thì bạn phải bật chế độ USB Debugging trên máy thật bằng cách vào Settings → General → Developer options và check vào dòng USB Debugging. Nếu máy bạn không có tùy chọn Developer options thì tức là chế độ này đã bị ẩn, bạn phải vào Settings → About phone → Software information rồi tìm đến dòng Build number và nhấp vào đó 7 lần, sau đó tùy chọn Developer options sẽ hiện ra.

Screenshot_2016-05-05-21-18-32

Chạy chương trình từ dòng lệnh

Để có thể biên dịch và chạy từ dòng lệnh thì bạn phải tải thêm trình ant tại địa chỉ http://ant.apache.org/bindownload.cgi, sau đó giải nén và thêm đường dẫn đến thư mục bin trong thư mục ant mà bạn vừa giải nén vào biến môi trường Path.

C:\Project\Android>ant debug install

Sau đó chạy lệnh ant debug install tại thư mục chứa project để dịch chương trình. Lệnh này sẽ biên dịch và tạo ra file có tên là <tên project>-debug.apk tại thư mục bin/, ở đây là file MyFirstApp-debug.apk do chúng ta đặt tên project là MyFirstApp.

C:\Project\Android>adb devices
List of devices attached
LGD325f7d0c658  device

Để chạy ứng dụng thì chúng ta tìm các máy Android hiện đang kết nối với máy tính. Hiện tại ở đây mình sử dụng máy thật để chạy, lệnh adb devices liệt kê máy mình có tên là “LGD325f7d0c658”.

C:\Project\Android>adb install -r bin/MyFirstApp-debug.apk

Lệnh adb install <tên file apk> sẽ cài file apk vào thiết bị để chạy, tham số -r cho biết nếu trên máy đã tồn tại ứng dụng rồi thì ghi đè lên. Nếu lệnh adb devices liệt kê ra nhiều thiết bị tức là hiện tại máy bạn có nhiều thiết bị android kết nối đến, thì trong lệnh adb install bạn phải chỉ ra cả tên thiết bị đó nữa bằng tham số -s phía sau lệnh adb, ví dụ adb -s "LGD325f7d0c658" install -r bin/MyFirstApp-debug.apk.

Screenshot_2016-05-06-14-35-05

Thế là xong, bạn có thể thấy project đã được cài trên máy mình và có thể nhấp vào đó để chạy.

Screenshot_2016-05-06-14-32-19

 

Android – Giới thiệu

Android là một hệ điều hành họ Linux được thiết kế để chạy trên các thiết bị di động như smartphone, máy tính bảng, smart TV, đồng hồ thông minh, nhà thông minh… Ngôn ngữ lập trình Android là một ngôn ngữ được phát triển từ Java.

Android được bắt đầu phát triển vào năm 2003 bởi một công ty ở California, sau đó công ty này được mua lại bởi Google. Từ đó Google phát triển “Dự án Android mã nguồn mở” nhằm phát triển hệ điều hành này. Android có tốc độ phát triển rất nhanh, hầu như các bản cập nhật đều được phát hành đều đặn. Mỗi phiên bản của Android lấy tên từ một loại đồ ăn ngọt, như Donut (bánh rán), Gingerbread (bánh gừng), Jelly Bean (kẹo ngọt)… Các lập trình viên Android có thể đăng các ứng dụng mà họ viết ra lên cửa hàng ứng dụng của Google có tên là Google Play và người dùng có thể tải về cài đặt và sử dụng.

Kiến trúc của hệ điều hành Android

Android được phát triển dựa trên Linux, tầng đầu tiên của kiến trúc Android. Bộ lõi của Linux chịu trách nhiệm xử lý các công việc liên quan đến quản lý tiến trình, quản lý bộ nhớ, driver, mạng…v.v

Tầng tiếp theo nằm phía trên bộ lõi của Linux là các thư viện cơ bản và bộ thư viện Android (tiếng Anh là Android runtime). Các thư viện này được viết bằng C/C++ có nhiệm vụ cung cấp các dịch vụ như lưu trữ dữ liệu, hiển thị giao diện, xử lý đa phương tiện, duyệt web… Bộ thư viện Android lại bao gồm 2 phần là máy ảo Dalvik và các thư viện Java. Dalvik là một máy ảo có nhiệm vụ chạy các chương trình viết bằng Java trên nền Android, Dalvik khác với máy ảo Java thông thường là JVM (Java Virtual Machine).

Tầng tiếp theo là application framework, đây là một tập các hàm API cung cấp các dịch vụ cho ứng dụng Android. Các hàm API này chịu trách nhiệm xử lý các tác vụ cấp cao như quản lý Activity, quản lý Notification, quản lý Resource, hiển thị View, quản lý Package… chúng ta sẽ làm việc chủ yếu với tầng này. Không những thế Android còn cung cấp sẵn một số ứng dụng thường dùng để lập trình viên có thể sử dụng hoặc tích hợp vào chương trình của mình như trình duyệt web, SMS, lịch, bản đồ, danh sách điện thoại liên lạc…v.v.

Một số khái niệm

Một chương trình Android được nén trong một file có đuôi mở rộng là .apk.

Mỗi chương trình Android chạy trong một môi trường ảo của riêng chúng.

Một Activity một thành phần trong một ứng dụng Android chịu trách nhiệm quản lý các hành động xảy ra trên một “màn hình”, ví dụ như hiển thị một màn hình, xử lý sự kiện nhấp button…v.v một Activity được kế thừa từ lớp Activity. 

Service là các chương trình chạy ngầm bên dưới hệ điều hành, thường không tương tác trực tiếp với người dùng, kế thừa từ lớp Service. 

Content Provider là các chương trình quản lý nội dung có nhiệm vụ truy xuất dữ liệu được lưu trong file, cơ sở dữ liệu SQLite hoặc lưu trên web. Được viết từ lớp ContentProvider.

Broadcast Receiver là các chương trình quản lý các thông báo từ hệ điều hành hoặc ứng dụng, được kế thừa từ lớp BroadcastReceiver. 

Các thông báo được gửi đi là một đối tượng Intent, đối tượng này lưu thông tin về các hành động được thực hiện, thường thì Intent được dùng để thực hiện các công việc như chuyển đổi qua lại giữa các Activity.

Một View là một lớp hiển thị giao diện người dùng, lớp này có nhiệm vụ vẽ giao diện và xử lý sự kiện. Từ lớp View gốc, các lớp Widget được tạo ra kế thừa từ lớp view có nhiệm vụ hiển thị các thành phần giao diện cụ thể hơn như button, check box… Ngoài lớp View còn có lớp ViewGroup tập hợp nhiều View lại để hiển thị theo một trật tự nào đó.

Mỗi ứng dụng Android đều phải có một file tên là AndroidManifest.xml trong thư mục gốc. File này cung cấp các thông tin về ứng dụng.

Cài đặt Android

Trước tiên chúng ta phải cài JDK trong máy đã, phần này bạn tự cài, ở đây phiên bản JDK mà mình sử dụng là phiên bản 1.8.0_77.

Tiếp theo chúng ta tải và cài bộ Android SDK tại địa chỉ http://developer.android.com/sdk/index.html#downloads. Ở đây phiên bản SDK mà mình sử dụng là phiên bản r24.4.1. Lưu ý trên website download có cả phần tải Android Studio, đây là một chương trình hỗ trợ phát triển ứng dụng Android một cách nhanh chóng nhưng cũng rất nặng (cả về dung lượng lẫn cấu hình), còn riêng Android SDK chỉ nặng khoảng 144 MB. Bạn cũng có thể cài Android Studio nếu muốn, ở đây mình chỉ cài bộ SDK.

Sau khi cài xong chúng ta nên thiết lập đường dẫn đến 2 thư mục tools/ và platform-tools/ trong thư mục cài đặt SDK vào biến môi trường PATH bằng cách vào: Computer → System Properties  Advanced System Properties  Enviroment Variables, một hộp thoại hiện lên, chúng ta tìm đến biến Path trong phần System Variables và thêm vào dấu chấm phẩy “;”, sau đó là đường dẫn đến thư mục tools/ rồi thêm một dấu chấm phẩy nữa, theo sau là đường dẫn đến thư mục platform-tools/. 

Sau đó chúng ta có thể chạy SDK Manager.exe (hoặc lệnh android trong Command Linecmd nếu bạn có thiết lập biến PATH) để mở trình Android SDK Manager lên. Đây là trình quản lý các phiên bản SDK cho từng phiên bản hệ điều hành và một số công cụ thường dùng. Thường thì chúng ta sẽ cài một số phiên bản API mới nhất thôi chứ không cài hết vì chúng có dung lượng rất nặng.

Capture

Android AVD

Để chạy các ứng dụng Android thì bạn phải có một chiếc máy Android như smartphone hay tablet để chạy, nhưng nếu không có thì bạn phải dùng máy ảo. Android AVD là trình quản lý máy ảo, cho phép chúng ta tạo các thiết bị Android ảo chạy trên máy tính.

Để chạy trình AVD thì chúng ta chạy file AVD Manager.exe hoặc lệnh android avd trong Command Prompt.

Capture

Sau đó bạn bấm vào nút Create để tiến hành tạo máy ảo, bạn chọn tên và các cấu hình cần thiết. Nếu bạn không rành về các thông số như thế nào thì có thể thiết lập theo hình trên.

Sau đó AVD sẽ liệt kê chiếc máy ảo mà chúng ta vừa tạo, chúng ta có thể bấm Start để chạy chiếc máy ảo trên.

Capture

Một lựa chọn thay thế khác là bạn có thể sử dụng máy ảo Genymotion vì máy ảo mặc định của Google chiếm rất nhiều tài nguyên và mất rất nhiều thời gian để khởi động. Tuy nhiên nếu được thì chúng ta nên kiếm một chiếc máy Android thật để sử dụng thì sẽ tốt hơn.