iT邦幫忙

2025 iThome 鐵人賽

DAY 25
0

A:Activity

MainActivity

  • 負責「註冊」、「登入」邏輯
  • UI 綁定、輸入監聽、資料檢查、跳轉頁面

HomeActivity

  • 負責登入後顯示資料
  • 若為 admin 可查看所有用戶,否則僅顯示個人資料

P:Persistence(資料儲存層)

LoginData 類別作為資料中心

  • 採用 Singleton 設計模式,確保只存在一個實例
  • 使用 ArrayList<HashMap<String, String>> 儲存使用者資料
  • 提供存取資料的接口(getData, setData

S:Singleton 單例模式

  • LoginData 用雙重鎖定(Double-Checked Locking)實作 thread-safe 的單例
  • 保證整個 app 共享同一份資料來源(模擬資料庫功能)
  • 不管在哪個 Activity 呼叫 LoginData.getInstance(),拿到的都是同一份資料,可以確保存取到的內容都一樣

優點

  • 簡潔好理解
    透過單例儲存資料,避免複雜的 Room / SQLite 架構。
  • 📦 擴充性高
    將來若要改為用 SQLite 或 Room,只需修改 LoginData。
  • 🧩 封裝性好
    使用 LoginData 集中管理帳號資料,Activity 不直接操控資料結構。

以下是完整程式碼及註解,另外,在這個範例中,我們「假裝」使用了資料庫,其實是用一個ArrayList來儲存假資料,也就是說資料並沒有真的存入硬碟或資料庫系統,而只是在記憶體中的一個資料結構裡暫存,但當 App 關閉或程式重啟時,記憶體中的資料會消失,不會保存到磁碟上,因此不算是真正意義上的資料庫

1. Activity

1. MainActivity

public class MainActivity extends AppCompatActivity {

    private Button saveButton, loginButton;
    private EditText emailEditText, usernameEditText, passwordEditText;
    private LoginData loginData = LoginData.getInstance(); // 連接 LoginData

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

        bindUI();
    }

    protected void bindUI() {
        saveButton = findViewById(R.id.main_save_btn);
        loginButton = findViewById(R.id.main_login_btn);
        emailEditText = findViewById(R.id.main_email_et);
        usernameEditText = findViewById(R.id.main_username_et);
        passwordEditText = findViewById(R.id.main_password_et);

        // TextWatcher: 監聽輸入框,輸入框內容一改就自動檢查
        TextWatcher watcher = new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) { }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                check();
            }

            @Override
            public void afterTextChanged(Editable s) { }
        };
        // 讓 email、username、password 這三個 EditText 的內容一有變動,都會自動呼叫 check()
        emailEditText.addTextChangedListener(watcher);
        usernameEditText.addTextChangedListener(watcher);
        passwordEditText.addTextChangedListener(watcher);

        // 一開始就檢查一次
        check();

        // 將點擊事件獨立出去
        saveButton.setOnClickListener(this::onSaveButtonClick);
        loginButton.setOnClickListener(this::onLoginButtonClick);

        // 預先建立管理員帳號
        if (loginData.getData().isEmpty()) {
            HashMap<String, String> admin = new HashMap<>();
            admin.put("username", "admin");
            admin.put("email", "admin@gmail.com");
            admin.put("password", "123456");
            loginData.getData().add(admin);
        }
    }

    // 清空輸入欄位
    private void clear() {
        emailEditText.setText("");
        usernameEditText.setText("");
        passwordEditText.setText("");
        emailEditText.setError(null);
        passwordEditText.setError(null);
    }

    private void check() {
        String username = usernameEditText.getText().toString();
        String email = emailEditText.getText().toString();
        String password = passwordEditText.getText().toString();

        boolean isAllFilled = !email.isEmpty() && !password.isEmpty() && !username.isEmpty();
        boolean isEmail = Patterns.EMAIL_ADDRESS.matcher(email).matches();
        boolean isPassword = Pattern.matches("\\d{6,}", password); // \\d:必須是數字 {6,}:至少連續6個字

        emailEditText.setOnFocusChangeListener((v, hasFocus) -> {
            if (!hasFocus) {
                if (!isEmail && !email.isEmpty()) {
                    emailEditText.setError("電子郵件格式錯誤");
                } else {
                    emailEditText.setError(null);
                }
            }
        });

        passwordEditText.setOnFocusChangeListener((v, hasFocus) -> {
            if (!hasFocus) {
                if (!isPassword && !password.isEmpty()) {
                    passwordEditText.setError("密碼需至少6碼數字");
                } else {
                    passwordEditText.setError(null);
                }
            }
        });

        saveButton.setEnabled(isAllFilled && isEmail && isPassword);
        loginButton.setEnabled(isAllFilled && isEmail && isPassword);
    }

    private void onSaveButtonClick(View view) {
        String username = usernameEditText.getText().toString();
        String email = emailEditText.getText().toString();
        String password = passwordEditText.getText().toString();

        check();

        // 用 for 迴圈逐一檢查目前所有已經註冊的用戶資料(每個 user 是一個 HashMap)
        for (HashMap<String, String> user : loginData.getData()) {
            // 如果 username 重複
            if (username.equals(user.get("username"))) {
                Toast.makeText(this, "該用戶名稱已被占用", Toast.LENGTH_SHORT).show();
                return; // 直接結束,不新增
            }
            // 如果 email 重複
            if (email.equals(user.get("email"))) {
                Toast.makeText(this, "該 email 已被占用", Toast.LENGTH_SHORT).show();
                return; // 直接結束,不新增
            }
        }

        if (!username.isEmpty() && !email.isEmpty() && !password.isEmpty()) {
            // 沒有重複且欄位都有填寫才新增
            HashMap<String, String> newUser = new HashMap<>();
            newUser.put("username", username);
            newUser.put("email", email);
            newUser.put("password", password);
            loginData.getData().add(newUser);
            clear();
            Toast.makeText(this, "已儲存資料", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "請填寫完整", Toast.LENGTH_SHORT).show();
        }
    }

    private void onLoginButtonClick(View view) {
        String username = usernameEditText.getText().toString();
        String email = emailEditText.getText().toString();
        String password = passwordEditText.getText().toString();

        check();

        if (!username.isEmpty() && !email.isEmpty() && !password.isEmpty()) {
            // 比較 LoginData 物件中的資料,和畫面上用戶在欄位中輸入的文字是否一樣(儲存多筆資料)
            boolean found = false;
            for (HashMap<String, String> user : loginData.getData()) {
                if (user.get("email").equals(email) &&
                    user.get("password").equals(password) &&
                    user.get("username").equals(username)) {
                    found = true;
                    break;
                }
            }

            // 如果找到對應資料
            if (found) {
                // 登入
                Intent intent = new Intent(this, HomeActivity.class);
                // 傳輸資料
                intent.putExtra("username", username);
                startActivity(intent);
                Toast.makeText(this, "Hello,"+username, Toast.LENGTH_SHORT).show();
                clear();
            } else {
                // 登入失敗
                clear();
                Toast.makeText(this, "登入失敗", Toast.LENGTH_SHORT).show();
            }
        } else {
            Toast.makeText(this, "請填寫完整", Toast.LENGTH_SHORT).show();
        }
    }
}

2. HomeActivity

public class HomeActivity extends AppCompatActivity {

    private Button getdataButton, backButton;
    private TextView showdataTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_home);

        bindUI();
    }

    protected void bindUI() {
        getdataButton = findViewById(R.id.home_getdata_btn);
        backButton = findViewById(R.id.home_back_btn);
        showdataTextView = findViewById(R.id.home_showdata_tv);

        // 取得 MainActivity 傳來的 Intent
        Intent intent = getIntent();
        String username = intent.getStringExtra("username");

        getdataButton.setOnClickListener(view -> {
            LoginData loginData = LoginData.getInstance();
            StringBuilder sb = new StringBuilder();

            if ("admin".equals(username)) {
                int index = 1;
                for (HashMap<String, String> user : loginData.getData()) {
                    sb.append(String.format("帳號%d:\n用戶名:%s\n電子郵件:%s\n密碼:%s\n\n",
                        index++,
                        user.get("username"),
                        user.get("email"),
                        user.get("password")));
                }
            } else {
                // 只顯示自己
                for (HashMap<String, String> user : loginData.getData()) {
                    if (username.equals(user.get("username"))) {
                        sb.append(String.format("用戶名:%s\n電子郵件:%s\n密碼:%s\n",
                            user.get("username"),
                            user.get("email"),
                            user.get("password")));
                        break;
                    }
                }
            }
            showdataTextView.setText(sb.toString());
        });

        backButton.setOnClickListener(v -> finish());
    }
}

3. LoginData

public class LoginData {
    private static LoginData instance; // 單例實例變數,用來保存唯一的 LoginData 物件
    private String username; // 用戶名
    private String email; // 電子郵件
    private String password; // 密碼
    private String phone; // 電話
    private ArrayList<HashMap<String, String>> data = new ArrayList<HashMap<String, String>>();

    private LoginData() {}  // 私有建構子,外部不能用 new LoginData() 新建物件,只能透過 getInstance() 取得唯一實例

    public static LoginData getInstance() {
        if (instance == null) {
            synchronized (LoginData.class) {
                if (instance == null) {
                    instance = new LoginData();
                }
            }
        }
        return instance;
    }

    // getter 和 setter 方法
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
    public String getPhone() { return phone; }
    public void setPhone(String phone) { this.phone = phone; }

    public ArrayList<HashMap<String, String>> getData() {
        return data;
    }
    public void setData(ArrayList<HashMap<String, String>> data) {
        this.data = data;
    }
}

2. XML

1. activity_main

<?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:layout_marginTop="40dp"
    android:orientation="vertical"
    android:padding="16dp">

    <EditText
        android:id="@+id/main_email_et"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Email"
        android:inputType="textEmailAddress" />

    <EditText
        android:id="@+id/main_username_et"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Username"
        android:inputType="text" />

    <EditText
        android:id="@+id/main_password_et"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Password"
        android:inputType="textPassword" />

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

        <Button
            android:id="@+id/main_save_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="儲存" />

        <Button
            android:id="@+id/main_login_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="登入" />

    </LinearLayout>
</LinearLayout>

2. activity_home

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp"
    android:gravity="center_horizontal">

    <TextView
        android:id="@+id/home_showdata_tv"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:gravity="center_horizontal|top"
        android:paddingTop="40dp"
        android:text="資料顯示處"
        android:textSize="20sp" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:paddingTop="24dp"
        android:paddingStart="40dp"
        android:paddingEnd="40dp"
        android:gravity="center_horizontal">

        <Button
            android:id="@+id/home_getdata_btn"
            android:layout_width="wrap_content"
            android:layout_height="70dp"
            android:text="獲取資料"
            android:textSize="20sp"
            android:layout_marginEnd="24dp"/>

        <Button
            android:id="@+id/home_back_btn"
            android:layout_width="wrap_content"
            android:layout_height="70dp"
            android:text="返回上頁"
            android:textSize="20sp"/>

    </LinearLayout>
</LinearLayout>

檔案結構圖:
image


上一篇
Day 24.MVP架構
下一篇
Day 26.APS架構(SharedPreferences)
系列文
Android 新手的 30 天進化論:從初學者到小專案開發者30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言