Category Archives: Java

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.

Java Swing – Drag và Drop

Trong lập trình GUI thì Drag và Drop (tiếng việt là Kéo Thả) là hành động click và giữ chuột lên một đối tượng rồi “kéo” đối tượng đó lên một vị trí khác hoặc lên một đối tượng khác.

Kéo thả là một trong những tính năng thường thấy trong GUI, cho phép người dùng thực hiện các công việc phức tạp.

Chúng ta có thể kéo thả dữ liệu hoặc các đối tượng có hình thù cụ thể, ví dụ như chúng ta kéo một file ảnh vào cửa sổ chat để gửi file thì đó là kéo dữ liệu, hoặc kéo các tab trong trình duyệt Chrome là kéo đối tượng có hình thù.

Hầu hết các component trong Java Swing đều có phương thức hỗ trợ kéo thả, và chúng ta cũng có thể viết các phương thức kéo thả của riêng chúng ta.

Ví dụ

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JTextField;
import javax.swing.TransferHandler;


public class Example extends JFrame {

    JTextField field;
    JButton button;

    public Example() {

        setTitle("Drag And Drop Example");

        setLayout(null);

        button = new JButton("Button");
        button.setBounds(200, 50, 90, 25);

        field = new JTextField();
        field.setBounds(30, 50, 150, 25);

        add(button);
        add(field);

        field.setDragEnabled(true);
        button.setTransferHandler(new TransferHandler("text"));

        setSize(330, 150);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
        setVisible(true);
    }

    public static void main(String[] args) {
        new Example();
    }
}

Chúng ta hiển thị một JTextField và một JButton, đoạn text trong text field có thể kéo sang làm text cho button.

field.setDragEnabled(true);

Để dữ liệu của một component có thể kéo được thì chúng ta phải thiết lập trong phương thức setDragEnabled() vì mặc định Swing đã vô hiệu hóa tính năng này.

button.setTransferHandler(new TransferHandler("text"));

Lớp TransferHandler là trái tim của Drag và Drop trong Swing, lớp này có nhiệm vụ vận chuyển dữ liệu qua lại giữa các component, tham số của lớp này là một thuộc tính Bean, nếu bạn chưa biết gì về các thuộc tính bean thì chỉ cần nhớ rằng bất kì thuộc tính nào có phương thức getter  setter đều có thể đưa vào làm tham số cho TransferHandler.

Chẳng hạn như JButton có phương thức getText()setText(), do đó bạn có thể đưa tham số là "text", vì khi chúng ta thiết lập kiểu dữ liệu là "text", TransferHandler sẽ dùng các phương thức getter  setter tương ứng để lấy và nhập dữ liệu với các component. Và vì chúng ta thiết lập kiểu text nên bạn chỉ có thể kéo hoặc thả các chuỗi text vào ra component này thôi.

Capture

Tùy chỉnh khả năng kéo

Không phải tất cả các component trong Swing đều có thể kéo được, JLabel là một ví dụ, chúng ta phải code phương thức kéo riêng. Ở đây chúng ta sẽ thực hiện kéo thả thuộc tính icon.

import java.awt.FlowLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.TransferHandler;


public class Example extends JFrame {


    public Example() {

        setTitle("Drag And Drop Example");

        JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 50, 15));

        ImageIcon icon1 = new ImageIcon("C:/sad.png");
        ImageIcon icon2 = new ImageIcon("C:/smile.png");
        ImageIcon icon3 = new ImageIcon("C:/crying.png");

        JButton button = new JButton(icon2);
        button.setFocusable(false);

        JLabel label1 = new JLabel(icon1, JLabel.CENTER);
        JLabel label2 = new JLabel(icon3, JLabel.CENTER);

        MouseListener listener = new DragMouseAdapter();
        label1.addMouseListener(listener);
        label2.addMouseListener(listener);

        label1.setTransferHandler(new TransferHandler("icon"));
        button.setTransferHandler(new TransferHandler("icon"));
        label2.setTransferHandler(new TransferHandler("icon"));

        panel.add(label1);
        panel.add(button);
        panel.add(label2);
        add(panel);

        pack();
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
        setVisible(true);
    }

    class DragMouseAdapter extends MouseAdapter {
        public void mousePressed(MouseEvent e) {
            JComponent c = (JComponent) e.getSource();
            TransferHandler handler = c.getTransferHandler();
            handler.exportAsDrag(c, e, TransferHandler.COPY);
        }
    }

    public static void main(String[] args) {
        new Example();
    }
}

Chúng ta hiển thị 2 label và một button, cả 3 component này đều hiển thị icon, chúng ta có thể kéo icon từ 2 label vào làm icon cho button.

MouseListener listener = new DragMouseAdapter();
label1.addMouseListener(listener);
label2.addMouseListener(listener);

Như đã nói, JLabel không được hỗ trợ tính năng kéo, hay nói cách khác là không có phương thức setDragEnabled(), nên ở đây chúng ta tự gắn một MouseAdapter vào để mô phỏng sự kiện kéo.

label1.setTransferHandler(new TransferHandler("icon"));
button.setTransferHandler(new TransferHandler("icon"));
label2.setTransferHandler(new TransferHandler("icon"));

Cả 3 component của chúng ta đều có các phương thức getter/setter cho thuộc tính icon. Đối với các lớp có sẵn phương thức hỗ trợ kéo là setDragEnabled() thì bạn có thể không cần phải chỉ ra kiểu dữ liệu vận chuyển trong TransferHandler và Swing sẽ vận chuyển các dữ liệu mặc định (chẳng hạn như text đối với JTextField), còn với các lớp không có sẵn thì chúng ta phải chỉ ra kiểu dữ liệu rõ ràng trong phương thức setTransferHandler().

JComponent c = (JComponent) e.getSource();
TransferHandler handler = c.getTransferHandler();
handler.exportAsDrag(c, e, TransferHandler.COPY);

Ba dòng code trên thiết lập khả năng kéo cho JLabel bằng phương thức exportAsDrag(), phương thức này nhận vào đối tượng được kéo đi (e.getSource()), dữ liệu về sự kiện kéo chuột (e) và cách mà dữ liệu được, ở đây là TransferHandler.COPY, tức là dữ liệu từ component nguồn sẽ được copy sang đối tượng đích. Ngoài copy thì chúng ta còn có một số thao tác khác như MOVE, NONE, MOVE_OR_COPY, COPY, LINK.

Capture12

Tùy chỉnh khả năng thả

Nếu có một số component không có sẵn phương thức hỗ trợ kéo thì cũng có một số component không có phương thức hỗ trợ thả, JList là một ví dụ. Lý do là bởi vì khi chúng ta kéo thả vào JList thì Swing không biết chúng ta muốn thả vào như thế nào, vd như insert vào đầu list, cuối list hay giữa list hay ở vị trí bất kì nào…

import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;

import javax.swing.DefaultListModel;
import javax.swing.DropMode;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.TransferHandler;


public class Example extends JFrame {

    JTextField field;
    DefaultListModel model;

    public Example() {

        setTitle("Drag And Drop Example");

        JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 15, 15));
  
        JScrollPane pane = new JScrollPane();
        pane.setPreferredSize(new Dimension(180, 150));

        model = new DefaultListModel();
        JList list = new JList(model);

        list.setDropMode(DropMode.INSERT);
        list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        list.setTransferHandler(new ListHandler());

        field = new JTextField("");
        field.setPreferredSize(new Dimension(150, 25));
        field.setDragEnabled(true);

        panel.add(field);
        pane.getViewport().add(list); 
        panel.add(pane);

        add(panel);

        pack();

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
        setVisible(true);
    }


    private class ListHandler extends TransferHandler {
        public boolean canImport(TransferSupport support) {
            if (!support.isDrop()) {
                return false;
            }

            return support.isDataFlavorSupported(DataFlavor.stringFlavor);
        }

        public boolean importData(TransferSupport support) {
            if (!canImport(support)) {
                return false;
            }

            Transferable transferable = support.getTransferable();
            String line;
            try {
                line = (String) transferable.getTransferData(DataFlavor.stringFlavor);
            } catch (Exception e) {
                return false;
            }

            JList.DropLocation dl = (JList.DropLocation) support.getDropLocation();
            int index = dl.getIndex();

            String[] data = line.split(",");
            for (String item: data) {
                if (!item.isEmpty())
                    model.add(index++, item.trim());
            }
            return true;
       }
    }

    public static void main(String[] args) {
        new Example();
    }
}

Chúng ta sử dụng một JTextField và một JList, các đoạn text trong JTextField có thể được kéo qua JList, đoạn text sẽ dược phân ra thành nhiều text con bằng dấu phẩy.

list.setDropMode(DropMode.INSERT);

Phương thức setDropMode() thiết lập cách dữ liệu được thả vào, ở đây là DropMode.INSERT, tức là chèn vào cuối list.

list.setTransferHandler(new ListHandler());

Mặc dù chúng ta chỉ kéo thả các đoạn text bình thường từ JTextField sang nhưng vì JList không có các phương thức getter/setter tương ứng nên chúng ta phải định nghĩa một lớp TransferHandler riêng là ListHandler.

public boolean canImport(TransferSupport support) {
    if (!support.isDrop()) {
        return false;
    }
    return support.isDataFlavorSupported(DataFlavor.stringFlavor);
}

Khi chúng ta kéo dữ liệu sang một component (chỉ kéo thôi chứ chưa thả), phương thức canImport() của đối tượng TransferHandler đó sẽ được gọi liên tục, phương thức này nhận một đối tượng TransferSupport do Swing tự tạo ra, phương thức này sẽ kiểm tra xem dữ liệu được chuyển sang có được nhận hay không. Chúng ta override lại phương thức này trong lớp ListHandler.

Ở đây chúng ta dùng phương thức isDrop() kiểm tra xem người dùng đã thả dữ liệu ra chưa hay vẫn còn giữ chuột để tiếp tục kéo. Phương thức isDataFlavorSupported() kiểm tra xem dữ liệu được chuyển sang có được hỗ trợ hay không, phương thức này nhận vào một đối tượng lớp DataFlavor, lớp này lưu trữ thông tin về các loại dữ liệu khác nhau, ở đây DataFlavor.stringFlavor lưu thông tin về kiểu string, ngoài ra còn một số kiểu khác như imageFlavor, allHtmlFlavor… bạn có thể tìm hiểu thêm tại đây.

public boolean importData(TransferSupport support) {
...
}

Phương thức importData() trong lớp TransferHandler sẽ được gọi khi người dùng thả chuột ra, tức là thả dữ liệu lên component, phương thức này nhận một đối tượng TransferSupport do Swing tự tạo ra, chúng ta cũng override lại phương thức này.

Transferable transferable = support.getTransferable();

Bên trong đối tượng TransferSupport có chứa một đối tượng Transferable, đây là đối tượng chứa thông tin về dữ liệu được vận chuyển.

line = (String) transferable.getTransferData(DataFlavor.stringFlavor);

Để lấy dữ liệu text thì chúng ta dùng phương thức getTransferData() và đưa vào tham số DataFlavor tương ứng.

JList.DropLocation dl = (JList.DropLocation) support.getDropLocation();
int index = dl.getIndex();

Bên trong đối tượng TranferSupport còn chứa một đối tượng TransferHandler.DropLocation, chúng ta lấy ra từ phương thức getDropLocation(), lớp này chứa thông tin về tọa độ chuột mà dữ liệu được thả.

Lớp JList cũng có một lớp DropLocation riêng kế thừa từ lớp này, JList.DropLocation sẽ dựa vào tọa độ được thả ra mà tính vị trí chỉ số trong danh sách các item, chúng ta lấy chỉ số này thông qua phương thức getIndex().

String[] data = line.split(",");
for (String item: data) {
    if (!item.isEmpty())
        model.add(index++, item.trim());
}

Cuối cùng chúng ta phân tích đoạn text được gửi sang rồi insert vào JList.

Capture-13

Java Swing – Mô hình MVC

Trong phần này chúng ta sẽ tìm hiểu về mô hình mà các component trong Java Swing sử dụng.

Bộ thư viện Swing được thiết kế dựa theo mô hình MVC (Model View Controller) cho phép thao tác với dữ liệu một cách tiện lợi và nhanh chóng.

Mô hình MVC

Mô hình MVC mà chúng ta thường thấy chia ứng dụng làm 3 phần: mô hình (Model), View và Controller. Trong đó model đại diện cho dữ liệu trong ứng dụng, view làm nhiệm vụ định dạng hiển thị cho dữ liệu và cuối cùng Controller xử lý sự kiện, có thể là sự kiện từ người dùng hoặc cũng chính nó tự sinh ra sự kiện. Nguyên gốc của mô hình MVC xuất phát từ ý tưởng chia tầng dữ liệu và tầng giao diện bằng một tầng ở giữa là Controller.

Thư viện Swing thì khác một tí, cả 2 phần View và Controller được gom lại làm một và được đặt tên là UI.

Trong Swing, tất cả mọi component đều có mô hình (model) của riêng nó, các mô hình lại được chia làm 2 loại là mô hình trạng thái và mô hình dữ liệu. Mô hình trạng thái lưu trữ trạng thái của component, ví dụ mô hình lưu giữ trạng thái của một button là đang được click hay đang được thả. Mô hình dữ liệu lưu trữ dữ liệu của component, chẳng hạn như một label lưu dữ liệu về text mà nó đang hiển thị.

Bản thân các component trong Swing đều được kế thừa từ một lớp model của riêng chúng, chẳng hạn như JButton kế thừa từ JAbstractButton, bên trong JAbstractButton có một thuộc tính là một đối tượng ButtonModel (bạn có thể tìm đọc trong bộ source của Java Swing), đối tượng này cung cấp các phương thức để lấy dữ liệu, thường thì nếu chúng ta muốn lấy dữ liệu thì chúng ta phải lấy đối tượng model này, nhưng lớp JButton thì lại có các phương thức lấy dữ liệu trực tiếp từ model luôn, nên bạn có thể lấy dữ liệu trực tiếp từ JButton mà không cần phải lấy đối tượng model trước.

public int getValue() { 
    return getModel().getValue(); 
}

Ví dụ như phương thức getValue() ở trên (đây là đoạn code phương thức getValue() của một component trong mã nguồn Java Swing), chúng ta lấy dữ liệu trực tiếp luôn chứ không cần phải lấy một đối tượng component nào đó rồi mới lấy dữ liệu. Hơn nữa việc thao tác trực tiếp với component là không an toàn.

Khi một component được tạo thì mặc định một đối tượng model sẽ được tạo, chẳng hạn như với JButton thì lớp đại diện cho model của nó là DefaultButtonModel.

public JButton(String text, Icon icon) {
  // Create the model
  setModel(new DefaultButtonModel());

  // initialize
  init(text, icon);
}

Ở trên là đoạn code khởi tạo JButton trong bộ mã nguồn của Java Swing.

Model của JButton

Mô hình được tạo sẵn cho JButton còn được dùng cho một số component phổ biến khác như check box, radio box, menu item… Loại mô hình của các component này là mô hình trạng thái.

import java.awt.Container;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;

import javax.swing.AbstractAction;
import javax.swing.DefaultButtonModel;
import javax.swing.GroupLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class ButtonModel extends JFrame {
 
    private JButton okbtn;
    private JLabel enabledLbl;
    private JLabel pressedLbl;
    private JCheckBox cb;
 
    public ButtonModel()
    {
        initUI();
    }
 
    private void initUI()
    {
        Container pane = getContentPane();
        GroupLayout gl = new GroupLayout(pane);
        pane.setLayout(gl);

        okbtn = new JButton("OK");
        okbtn.addChangeListener(new DisabledChangeListener());
        cb = new JCheckBox();
        cb.setAction(new CheckBoxAction());

        enabledLbl = new JLabel("Enabled: true");
        pressedLbl = new JLabel("Pressed: false");
 
        gl.setAutoCreateContainerGaps(true);
        gl.setAutoCreateGaps(true);

        gl.setHorizontalGroup(gl.createParallelGroup()
            .addGroup(gl.createSequentialGroup()
                .addComponent(okbtn)
                .addGap(80)
                .addComponent(cb))
            .addGroup(gl.createParallelGroup()
                .addComponent(enabledLbl)
                .addComponent(pressedLbl)) 
        );

        gl.setVerticalGroup(gl.createSequentialGroup()
            .addGroup(gl.createParallelGroup()
                .addComponent(okbtn)
                .addComponent(cb))
                .addGap(40)
            .addGroup(gl.createSequentialGroup()
                .addComponent(enabledLbl)
                .addComponent(pressedLbl))
        );

        pack();

        setTitle("ButtonModel");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }
 
    private class DisabledChangeListener implements ChangeListener {

        @Override
        public void stateChanged(ChangeEvent e) {

            DefaultButtonModel model = (DefaultButtonModel) okbtn.getModel();

            if (model.isEnabled()) {
                enabledLbl.setText("Enabled: true");
            } else {
                enabledLbl.setText("Enabled: false");
            }

            if (model.isPressed()) {
                pressedLbl.setText("Pressed: true");
            } else { 
                pressedLbl.setText("Pressed: false");
            }
        }
    }
 
    private class CheckBoxAction extends AbstractAction {
 
        public CheckBoxAction() {
           super("Disabled");
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            if (okbtn.isEnabled()) {
                okbtn.setEnabled(false);
            } else {
                okbtn.setEnabled(true);
            }
        }
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                ButtonModel ex = new ButtonModel();
                ex.setVisible(true);
            }
        });
    }
}

Chúng ta tạo một button, một checkbox và 3 label, các label này hiển thị dữ liệu về trạng thái của button như có đang được click hay không, đang bật hay đang bị vô hiệu hóa…

okbtn.addChangeListener(new DisabledChangeListener());

Chúng ta dùng lớp ChangeListener để lắng nghe sự thay đổi trạng thái của button.

DefaultButtonModel model = (DefaultButtonModel) okbtn.getModel();

Để lấy đối tượng model thì chúng ta dùng phương thức getModel().

if (model.isEnabled()) {
    enabledLbl.setText("Enabled: true");
} else {
    enabledLbl.setText("Enabled: false");
}

Chúng ta dùng phương thức isEnabled() để biết button đang dùng được hay đang bị vô hiệu hóa và cập nhật lên label.

if (okbtn.isEnabled()) {
    okbtn.setEnabled(false);
} else {
    okbtn.setEnabled(true);
}

Check box sẽ bật/tắt button nhưng ở đây chúng ta không dùng model mà dùng phương thức setEnabled() để thay đổi trực tiếp luôn.

public void setEnabled(boolean b) {
    if (!b && model.isRollover()) {
        model.setRollover(false);
    } 
    super.setEnabled(b);
    model.setEnabled(b);
}

Đoạn code trên là mã nguồn của phương thức JCheckBox.setEnable(), như bạn thấy thực chất khi gọi phương thức này JCheckBox tự động lấy model của nó và thiết lập thông tin từ model đó.

Capture

Tùy chỉnh Model

Trong phần này chúng ta làm lại những gì đã làm ở trên, chỉ khác là chúng ta sẽ sử dụng model do chúng ta tự thiết kế.

import java.awt.Container;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;
import javax.swing.ButtonModel;
import javax.swing.DefaultButtonModel;
import javax.swing.GroupLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import static javax.swing.JFrame.EXIT_ON_CLOSE;
import javax.swing.JLabel;


public class ButtonModel2 extends JFrame {

    private JButton okbtn;
    private JLabel enabledLbl;
    private JLabel pressedLbl;
    private JCheckBox cb;

    public ButtonModel2() {

        initUI();
    }
    
    private void initUI() {
        
        Container pane = getContentPane();
        GroupLayout gl = new GroupLayout(pane);
        pane.setLayout(gl);       
        
        okbtn = new JButton("OK");
        cb = new JCheckBox();
        cb.setAction(new CheckBoxAction());

        enabledLbl = new JLabel("Enabled: true");
        pressedLbl = new JLabel("Pressed: false");

        OkButtonModel model = new OkButtonModel();
        okbtn.setModel(model);        

        gl.setAutoCreateContainerGaps(true);
        gl.setAutoCreateGaps(true);

        gl.setHorizontalGroup(gl.createParallelGroup()
                .addGroup(gl.createSequentialGroup()
                        .addComponent(okbtn)
                        .addGap(80)
                        .addComponent(cb))
                .addGroup(gl.createParallelGroup()
                        .addComponent(enabledLbl)
                        .addComponent(pressedLbl))
        );

        gl.setVerticalGroup(gl.createSequentialGroup()
                .addGroup(gl.createParallelGroup()
                        .addComponent(okbtn)
                        .addComponent(cb))
                .addGap(40)
                .addGroup(gl.createSequentialGroup()
                        .addComponent(enabledLbl)
                        .addComponent(pressedLbl))
        );

        pack();

        setTitle("MVC");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }
    
    private class OkButtonModel extends DefaultButtonModel {

        @Override
        public void setEnabled(boolean b) {
            if (b) {
                enabledLbl.setText("Enabled: true");
            } else {
                enabledLbl.setText("Enabled: false");
            }

            super.setEnabled(b);
        }

        @Override
        public void setPressed(boolean b) {
            if (b) {
                pressedLbl.setText("Pressed: true");
            } else {
                pressedLbl.setText("Pressed: false");
            }

            super.setPressed(b);
        }
    }
    
    private class CheckBoxAction extends AbstractAction {
        
        public CheckBoxAction() {
            super("Disabled");
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            if (okbtn.isEnabled()) {
                okbtn.setEnabled(false);
            } else {
                okbtn.setEnabled(true);
            }
        }
    }    

    public static void main(String[] args) {
        
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                ButtonModel2 ex = new ButtonModel2();
                ex.setVisible(true);
            }
        });
    }
}

Chúng ta thiết kế lại giao diện như trong ví dụ trước, chỉ khác là chúng ta không dùng đến các listener nữa mà sử dụng đối tượng model kế thừa do chúng ta tự thiết kế.

ButtonModel model = new OkButtonModel();
okbtn.setModel(model); 

Để thiết lập model thì chúng ta sử dụng phương thức setModel().

private class OkButtonModel extends DefaultButtonModel {
...
}

Chúng ta tạo model button riêng và override một số phương thức cần thiết.

@Override
public void setEnabled(boolean b) {
    if (b) {
        enabledLbl.setText("Enabled: true");
    } else {
        enabledLbl.setText("Enabled: false");
    }

    super.setEnabled(b);
}

Chúng ta override phương thức setEnabled() và cập nhật lại text trên label, sau đó chúng ta phải gọi phương thức setEnabled() của lớp cha để cập nhật sự thay đổi.

Component có 2 model

Có một số component có tới 2 model, JList là một ví dụ, 2 model của JListListModel à ListSelectionModel. Trong đó ListModel là một model dữ liệu, model này lưu trữ danh sách các item trong list, cũng như tính chất của các item đó, ListSelectionModel là một model trạng thái, lưu trữ trạng thái của các item, chẳng hạn như item thứ mấy đang được chọn bởi người dùng…

import java.awt.Container;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.DefaultListModel;
import javax.swing.GroupLayout;
import static javax.swing.GroupLayout.Alignment.CENTER;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.ListSelectionModel;

public class ListModels extends JFrame {

    private DefaultListModel model;
    private JList list;
    private JButton remallbtn;
    private JButton addbtn;
    private JButton renbtn;
    private JButton delbtn;

    public ListModels() {

        initUI();
    }

    private void createList() {

        model = new DefaultListModel();
        model.addElement("Amelie");
        model.addElement("Aguirre, der Zorn Gottes");
        model.addElement("Fargo");
        model.addElement("Exorcist");
        model.addElement("Schindler's list");

        list = new JList(model);
        list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

        list.addMouseListener(new MouseAdapter() {

            @Override
            public void mouseClicked(MouseEvent e) {

                if (e.getClickCount() == 2) {
                    int index = list.locationToIndex(e.getPoint());
                    Object item = model.getElementAt(index);
                    String text = JOptionPane.showInputDialog("Rename item", item);
                    String newitem = null;
                    if (text != null) {
                        newitem = text.trim();
                    } else {
                        return;
                    }

                    if (!newitem.isEmpty()) {
                        model.remove(index);
                        model.add(index, newitem);
                        ListSelectionModel selmodel = list.getSelectionModel();
                        selmodel.setLeadSelectionIndex(index);
                    }
                }
            }
        });
    }

    private void createButtons() {

        remallbtn = new JButton("Remove All");
        addbtn = new JButton("Add");
        renbtn = new JButton("Rename");
        delbtn = new JButton("Delete");

        addbtn.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {

                String text = JOptionPane.showInputDialog("Add a new item");
                String item = null;

                if (text != null) {
                    item = text.trim();
                } else {
                    return;
                }

                if (!item.isEmpty()) {
                    model.addElement(item);
                }
            }
        });

        delbtn.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent event) {

                ListSelectionModel selmodel = list.getSelectionModel();
                int index = selmodel.getMinSelectionIndex();
                if (index >= 0) {
                    model.remove(index);
                }
            }

        });

        renbtn.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {

                ListSelectionModel selmodel = list.getSelectionModel();
                int index = selmodel.getMinSelectionIndex();
                if (index == -1) {
                    return;
                }

                Object item = model.getElementAt(index);
                String text = JOptionPane.showInputDialog("Rename item", item);
                String newitem = null;

                if (text != null) {
                    newitem = text.trim();
                } else {
                    return;
                }

                if (!newitem.isEmpty()) {
                    model.remove(index);
                    model.add(index, newitem);
                }
            }
        });

        remallbtn.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                model.clear();
            }
        });

    }

    private void initUI() {

        createList();
        createButtons();
        JScrollPane scrollpane = new JScrollPane(list);

        Container pane = getContentPane();
        GroupLayout gl = new GroupLayout(pane);
        pane.setLayout(gl);

        gl.setAutoCreateContainerGaps(true);
        gl.setAutoCreateGaps(true);

        gl.setHorizontalGroup(gl.createSequentialGroup()
                .addComponent(scrollpane)
                .addGroup(gl.createParallelGroup()
                        .addComponent(addbtn)
                        .addComponent(renbtn)
                        .addComponent(delbtn)
                        .addComponent(remallbtn))
        );

        gl.setVerticalGroup(gl.createParallelGroup(CENTER)
                .addComponent(scrollpane)
                .addGroup(gl.createSequentialGroup()
                        .addComponent(addbtn)
                        .addComponent(renbtn)
                        .addComponent(delbtn)
                        .addComponent(remallbtn))
        );

        gl.linkSize(addbtn, renbtn, delbtn, remallbtn);

        pack();

        setTitle("JList models");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                ListModels ex = new ListModels();
                ex.setVisible(true);
            }
        });
    }
}

Trong ví dụ này, chúng ta hiển thị một JList và 4 button. Các button sẽ điều khiển JList.

model = new DefaultListModel();
model.addElement("Amelie");
model.addElement("Aguirre, der Zorn Gottes");
...

Đầu tiên chúng ta tạo một model cho JList và thêm các item vào đó.

list = new JList(model);
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

Tiếp theo chúng ta tạo một đối tượng JList và thiết lập chế độ chỉ cho phép chọn một item tại một thời điểm.

if (text != null) {
    item = text.trim();
} else {
    return;
}

if (!item.isEmpty()) {
    model.addElement(item);
}

Chúng ta chỉ thêm item vào list nếu item đó không rỗng.

ListSelectionModel selmodel = list.getSelectionModel();
int index = selmodel.getMinSelectionIndex();
if (index >= 0) {
    model.remove(index);
}

Đoạn code trên thực hiện xóa các item, đầu tiên chúng ta lấy ra số thứ tự của item đang được chọn bằng phương thức getMinSelectionIndex() từ model của JList. Sau đó dùng phương thức remove() để xóa item này.

Trong ví dụ này mình dùng cả 2 model và dùng cả 2 phương pháp là sử dụng model và dùng phương thức trực tiếp của component. Các phương thức add(), remove(), clear() là dùng trực tiếp phương thức của component, còn lấy item đang được chọn là lấy từ model.

Capture

Java – Kết nối cơ sở dữ liệu MySQL – Phần 2

Trong phần này chúng ta tiếp tục tìm hiểu các thao tác thêm, sửa, xóa với CSDL MySQL.

Trước tiên chúng ta cần có vài dòng dữ liệu mẫu.

CREATE TABLE IF NOT EXISTS Authors(Id INT PRIMARY KEY AUTO_INCREMENT, 
    Name VARCHAR(25)) ENGINE=InnoDB;

INSERT INTO Authors(Id, Name) VALUES(1, 'Jack London');
INSERT INTO Authors(Id, Name) VALUES(2, 'Honore de Balzac');
INSERT INTO Authors(Id, Name) VALUES(3, 'Lion Feuchtwanger');
INSERT INTO Authors(Id, Name) VALUES(4, 'Emile Zola');
INSERT INTO Authors(Id, Name) VALUES(5, 'Truman Capote');
INSERT INTO Authors(Id, Name) VALUES(100, 'Taylor Swift');
INSERT INTO Authors(Id, Name) VALUES(1912, 'Titanic');

INSERT – UPDATE – DELETE

Ở đây chúng ta dùng lớp PreparedStatement lưu trữ các câu truy vấn mẫu, khi nào cần thực thi thì chúng ta chỉ cần truyền tham số vào và chạy thay vì viết câu truy vấn trực tiếp lên CSDL.

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.logging.Level;
import java.util.logging.Logger;

public class JDBCExample {

    public static void main(String[] args) {

        Connection con = null;
        PreparedStatement pst = null;

        String url = "jdbc:mysql://localhost:3306/test";
        String user = "root";
        String password = "";

        try {

            String author = "J.K. Rowling";
            con = DriverManager.getConnection(url, user, password);
            // INSERT
            pst = con.prepareStatement("INSERT INTO Authors(Name) VALUES(?)");
            pst.setString(1, author);
            pst.executeUpdate();

            // UPDATE
            pst = con.prepareStatement("UPDATE Authors SET Name=? WHERE Id=?");
            pst.setString(1, "Christopher Paolini");
            pst.setInt(2, 100);
            pst.executeUpdate();

            // DELETE
            pst = con.prepareStatement("DELETE FROM Authors WHERE Id=?");
            pst.setInt(1, 1912);
            pst.executeUpdate();

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

        } finally {

            try {
                if (pst != null) {
                    pst.close();
                }
                if (con != null) {
                    con.close();
                }

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

Chúng ta thêm một dòng dữ liệu mới vào bảng Authors.

pst = con.prepareStatement("INSERT INTO Authors(Name) VALUES(?)");

Chúng ta tạo đối tượng PreparedStatement. Dùng lớp này sẽ an toàn hơn là thực thi trực tiếp các câu truy vấn lên CSDL bằng lớp Statement vì có thể chống được kiểu tấn công SQL Injection. Ở đây chúng ta có câu truy vấn mẫu và các tham số là ký tự ?, chúng ta phải truyền giá trị vào các tham số đó.

pst.setString(1, author);

Để truyền giá trị thì chúng ta dùng các phương thức thiết lập dữ liệu như setString(), tham số thứ nhất của phương thức này là kí tự ? thứ mấy và tham số thứ 2 là giá trị truyền vào. Ở đây chúng ta truyền vào dấu ? thứ 1 và sẽ truyền vào đó biến author, các phương thức như setInt() cũng tương tự.

pst.executeUpdate();

Sau khi đã có đầy đủ tham số cho câu truy vấn thì chúng ta thực thi câu truy vấn bằng phương thức executeUpdate(). Phương thức này dùng chung cho tất cả các loại truy vấn mà không trả về bảng dữ liệu nào như INSERT, UPDATE, DELETE, giá trị trả về của phương thức này là True hoặc False.

SELECT

Tiếp theo chúng ta sẽ lấy dữ liệu từ CSDL.

import java.sql.PreparedStatement;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.logging.Level;
import java.util.logging.Logger;

public class QueryExample {
    
    public static void main(String[] args) {

        Connection con = null;
        PreparedStatement pst = null;
        ResultSet rs = null;

        String url = "jdbc:mysql://localhost:3306/test";
        String user = "root";
        String password = "";

        try {
            
            con = DriverManager.getConnection(url, user, password);
            pst = con.prepareStatement("SELECT * FROM Authors");
            rs = pst.executeQuery();

            while (rs.next()) {
                System.out.print(rs.getInt(1));
                System.out.print(": ");
                System.out.println(rs.getString(2));
            }

        } catch (SQLException ex) {
                ex.printStackTrace();
        } finally {

            try {
                if (rs != null) {
                    rs.close();
                }
                if (pst != null) {
                    pst.close();
                }
                if (con != null) {
                    con.close();
                }

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

Chúng ta sẽ truy xuất dữ liệu từ bảng Authors.

pst = con.prepareStatement("SELECT * FROM Authors");
rs = pst.executeQuery();

Chúng ta thực thi câu lệnh SELECT bằng phương thức executeQuery(), phương thức này trả về một đối tượng ResultSet.

while (rs.next()) {
      System.out.print(rs.getInt(1));
      System.out.print(": ");
      System.out.println(rs.getString(2));
}

Trong đối tượng ResultSet có một con trỏ chỉ đến hàng hiện tại, mặc định con trỏ này chỉ đến hàng số 0, hàng đầu tiên chứa dữ liệu là hàng số 1, phương thức next() có tác dụng di chuyển con trỏ đó lên một hàng. Phương thức getInt()getString() sẽ lấy giá trị ở cột và chúng ta chỉ định với hàng hiện tại của con trỏ.

1: Jack London
2: Honore de Balzac
3: Lion Feuchtwanger
4: Emile Zola
5: Truman Capote
100: Christopher Paolini
1913: J.K. Rowling