不要忘记构建该应用程序app

In the first part of this series, I describe how and why I started building the apps named Call Mom and Call Dad. This part describes the initial work needed to build the first useful version, including some Java pointers. If you just want the summary and some links, go to the Summary section at the bottom.

Don’t forget to build that app 📲

For this task, I went for writing the app in Java. Learning techniques that were completely new to me, like Kotlin or React Native, were not the main focus for me at the time, though I have gotten into both of those later. So I installed Android Studio, launched it, and started taking baby-steps forward.

来自现代Java和C#领域,并迁移到Java 7(启动时,Android Studio中的默认Java版本)感觉就像是时光倒流。

Fortunately, I was able to remedy some of these by activating Java 8 compatibility in Android Studio, and setting minSdkVersion to API level 24 in the manifest file.

Navigating the unknowns

Android has been around for more than a decade now, and is a really mature platform for both users and developers. Unfortunately, this also means that there is a lot of outdated information out there on blogs, video tutorials and StackOverflow questions. Even in the official documentation there are contradictions, and ambiguities when it comes to how to implement specific things. The best practices for one version quickly become frowned upon, or turn deprecated in a newer version of the Android SDK. At the same time, developers are encouraged to always target the latest version.

Compatibility with earlier Android versions 🤜🤛

幸运的是,有一些兼容性库,使开发人员可以针对最前沿的设备并使用更新的功能,同时自动回退到旧的等效API或模拟旧设备上的新行为。 这样就解决了这个问题。 问题是它已经解决了两次。

Support libraries

When learning how to use the RecyclerView and the CardView to allow the user to pick the correct contact to call from a list, I did it according to what I could find in the official documentation, by adding references to the Support Libraries. All was good for a while, and I used the Support Libraries for a lot of different things, like showing notifications correctly on all supported Android versions.

Later, when I wanted to add persistent data storage, I had to add references to AndroidX. After a while, the compiler started complaining about conflicts between different RecyclerView implementations. The conflicts came from me referencing those classes in code, Android Studio asking to automatically add import statements, and me picking the wrong ones.

Android Jetpack

Lately, Android development has seen a number of improvements to architecture and standardized components for all kinds of things, like data storage, user interface elements, notifications, media and security. Separate from the platform APIs, the Android Jetpack suite also includes an updated take on how to do version compatibility. From the AndroidX Overview page:

AndroidX通过提供功能奇偶校验和新库来完全替代支持库。

This is all very nice, but the top search result for RecyclerView, for example, at the time of me writing this, still leads to the older version. It's something to be aware of.

If you are working on an app that depends on the older Support Libraries, there are ways to easily and automatically migrate to AndroidX. In my experience, automatic migration works fine. Also, newer versions of Android Studio tries to coerce (and even force) you to use the newer compatibility libraries.

为确保多个Android版本之间的用户体验一致,请考虑以下几点提示:

  • 让你的活动扩展AppCompat活动代替活动:
import androidx.appcompat.app.AppCompatActivity;

public class MyActivity extends AppCompatActivity {
}
  • 使用上下文兼容而不是打电话语境 methods directly, when suitable methods exist:
import androidx.core.content.ContextCompat;

// Start other activities from inside an activity like this:
ContextCompat.startActivity(this, intent, options);
// And not like this:
this.startActivity(intent, options)

// Get some resources from inside an activity like this:
Drawable picture = ContextCompat.getDrawable(this, R.drawable.pic);
// And not like this:
Drawable picture = getDrawable(R.drawable.pic);

// Check permissions like this:
int permissionState = ContextCompat.checkSelfPermission(this, CALL_PHONE);
// And not like this:
int permissionState = checkSelfPermission(CALL_PHONE);
  • 使用NotificationManagerCompat代替NotificationManager:
import androidx.core.app.NotificationManagerCompat;

// Get the Notification Manager like this:
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
// And not like this:
NotificationManager = getSystemService(NOTIFICATION_SERVICE);

Persistent data 💾

为了处理用户对要呼叫的联系人的选择以及通知频率,我需要永久存储数据,因此在应用程序重启之间不会丢失任何信息。 我还需要存储用户最近通话的时间,以便能够计算下一个通知的日期和时间。

At first, I went with a Room database to store everything, and ended up creating a lot of 一种syncTask solutions to actively read data when needed or write data after user input. This approach was what I could find when I searched for answers. However, using the LiveData approach is much more efficient and straight-forward for subscribing to changed data across an entire app. Also, a Room database might not be the best storage for every bit of data your app needs to store.

SharedPreferences

当存储非常简单的数据(例如单个字符串或数字值)时,将其保存在Room数据库中可能是过大的。 无法在UI线程中读取和写入Room数据,因此您必须使用实时数据,异步任务或其他异步机制来读取或存储值。

The SharedPreferences APIs provide a key-value store that you can use directly from your Activity code, without spawning worker threads or worrying about synchronization issues. To read data, start by calling the getSharedPreferences method.

// First open the preferences file called "prefs"
// If it doesn't exist, it gets created automatically
SharedPreferences prefs = getSharedPreferences("prefs", MODE_PRIVATE);

// Read an integer named "launches" from the preferences
// If that values doesn't exist, let it return zero
int numberOfAppLaunches = prefs.getInt("launches", 0);

// Read a string named "username"
// If that value doesn't exist, let it return null
String username = prefs.getString("username", null);

您的应用可以维护多个不同的首选项文件(第一个参数getSharedPreferences)分隔数据组。 在我的应用程序中,我没有使用该功能,但是它对于避免名称冲突很有用。

要更新您的应用共享首选项,您首先需要创建一个编辑对象,将新值放入编辑器,然后调用应用(),该更改将更改异步保存到首选项文件,而不会干扰UI线程。

// Open the preferences file called "prefs"
SharedPreferences prefs = getSharedPreferences("prefs", MODE_PRIVATE);

// Create an Editor
SharedPreferences.Editor prefsEditor = prefs.edit();

// Update the value
numberOfAppLaunches += 1;
prefsEditor.putInt("launches", numberOfAppLaunches);

// Save changes to the preferences file
prefsEditor.apply();

Room databases

For storing more complex data, you should consider the Room persistance library. This give you access to the lightweight database engine SQLite, hidden behind an abstraction layer that helps you focus on designing your data model instead of getting sidelined by more complex things like connections and SQL query syntax beyond simple SELECT queries. Combined with the LiveData architecture, you get a fully reactive data flow, based on the Observer pattern.

首先定义数据类。 每个数据类都被注释为@实体并转换为SQLite数据库中的单个表。 这是多么简单妈妈数据实体类可能如下所示:

import androidx.room.Entity;
import androidx.room.PrimaryKey;

@Entity
public class MomData {
    @PrimaryKey(autoGenerate = true)
    public long id;

    public String name;
    public String number;
}

然后定义您的数据访问方法。 这些是Java接口,注释为@道,并应反映应用程序中的每种数据用例,例如从数据库表中检索所有实例,通过id获取一个特定实例,搜索与某些输入匹配的实例,更新现有实例或将您的实体的实例添加到数据库中:

import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;

@Dao
public interface MomDao {
    @Query("SELECT * FROM MomData")
    MomData[] getAllMoms();

    @Query("SELECT * FROM MomData WHERE id = :id")
    MomData getMomById(long id);

    @Query("SELECT * FROM MomData WHERE name = :whatName")
    MomData[] getAllMomsWithName(String whatName);

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void addOrUpdate(MomData mom);
}

此数据访问界面可让您的应用:

  • 列出数据库中的所有妈妈getAllMoms方法使用其ID与getMomById方法列出所有具有特定名字的妈妈getAllMomsWithName方法使用相同的方法添加一个新妈妈或更新一个现有妈妈addOrUpdate方法; the onConflict的参数@插入注释告诉Room如果ID匹配现有行,则Room替换数据库中的行,如果ID匹配现有行,则创建一个新行妈妈数据对象是一个新对象

As you can see, some SQL knowledge is required for creating queries, and if you find yourself having a need for more complex JOIN or WHERE clauses, you might want to investigate other ORM solutions, like GreenDao which has a sofisticated QueryBuilder concept.

最后,您创建一个抽象类来扩展RoomDatabase类,可以为您正确处理连接:

import androidx.room.Database;
import androidx.room.RoomDatabase;

// Add all your app's entity classes to the entities array
@Database(entities = { MomData.class }, version = 1)
public abstract class CallMomDatabase extends RoomDatabase {
    // Create an abstract DAO getter for each DAO class
    public abstract MomDao getMomDao();
}

现在,要使用数据库,您需要创建一个RoomDatabase.Builder对象,它将创建数据库(如果尚不存在)并建立与数据库的连接:

// From inside a method in an Activity:
RoomDatabase.Builder<CallMomDatabase> builder =
    Room.databaseBuilder(this, CallMomDatabase.class, "callmomdb");
CallMomDatabase db = builder.build();

// Get a list of all moms
MomData[] allMoms = db.getMomDao().getAllMoms();

// Close the connection to clean up
db.close();

但是,不允许您从应用程序的UI线程执行任何数据库查询,这意味着不能从任何应用程序调用上面的代码。onClick类方法。

My first solution to this was to create a lot of AsyncTask implementations, to create new worker threads any time I needed to read from, or write to, the database. This mostly worked fine, but I had to think about thread synchronization issues myself, which is always a pain. I do not recommend building your app this way. When I found out about LiveData, database connectivity could be made much cleaner and more robust, by adding just a little bit more code.

LiveData – a Room with a View

Making sure that your app's views show the correct data from your model at all times can be tricky, especially when you have to take the Activity Lifecycle into consideration. Your Activity object can get created and destroyed, paused and resumed, at any time, outside of your control, even when the user does a simple thing like turning their phone from portrait to landscape orientation. To know when, and how, to save the view state and when to read it back is not completely trivial.

Luckily, Android Jetpack provides a concept of 大号ifecycle-Aware Components, that solves a large part of that problem. One such component is 大号iveData, that is used to wrap a mutable value (a simple value or an object) in a lifecycle-aware observable. Any observer, such as an Activity or a Fragment will receive updated values exactly when they need to, at the correct times in their lifecycle. Even though 大号iveData objects can be used with any type of data from any source, they are especially useful for dealing with entities living in a Room database.

首先,您需要重构道界面以利用实时数据机制。 您需要将所有需要观察的数据的返回类型包装在实时数据<>通用类。

import androidx.lifecycle.LiveData;

@Dao
public interface MomDao {
    @Query("SELECT * FROM MomData")
    LiveData<MomData[]> getAllMoms();

    // ...
}

接下来,您应该创建一个视图模型实现以包含视图需要呈现的所有数据。 您可以移动代码来构建您的数据库对象在这里,但如果您的应用有多个视图模型类,您可能希望将该代码移至某些帮助方法并实现Singleton模式。

import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;

public class MainActivityViewModel extends ViewModel {
    private LiveData<MomData[]> allMoms;
    private final CallMomDatabase database;

    public MainActivityViewModel() {
        RoomDatabase.Builder<CallMomDatabase> builder =
            Room.databaseBuilder(this, CallMomDatabase.class, "callmomdb");
            database = builder.build();
    }

    public LiveData<MomData[]> getAllMoms() {
        if (allMoms == null) {
            allMoms = database.getMomDao().getAllMoms();
        }
        return allMoms;
    }
}

Notice that database.close() is no longer called. This is because LiveData needs the database connection to stay open. Finally, in your Activity you need to create an Observer to listen to changes in your data, and update your view correspondingly. Targeting Java 8, the most readable way to do this is by using a Method Reference, in this case the this::allMomsChanged reference:

import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProviders;

public class MainActivity extends AppCompatActivity {
    private MainActivityViewModel model;

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

        // Get an instance of the view model
        model = ViewModelProviders.of(this).get(MainActivityViewModel.class);

        // Start observing the changes by telling what method to call
        // when data is first received and when data changes
        model.getAllMoms().observe(this, this::allMomsChanged);
    }

    private void allMomsChanged(@Nullable final MomData[] allMoms) {
        // This is where you update the views using the new data
        // passed into this method.
    }
}

之间的合作房间和实时数据确保只要数据库中的数据发生更改,allMomsChanged上面的方法被自动调用,以允许UI反映数据的变化。

Setting alarms ⏰

A reminder app, such as Call Mom and Call Dad, need to be able to alert the user at specific times, even if their device is sleeping, and the alerts need to work correctly even if the device is rebooted. There is a mechanism in android called the 一种larm Manager, which you can use to wake the app up and run code on a schedule. The 一种larmManager class has lots of different methods to set these alarms, and 一种larmManagerCompat can help you set alarms in a way that is consistent across Android versions. You need to be careful when selecting which method to use, because if you design your alarm badly, your app can drain the battery of a device.

Setting the alarm

我决定使用报警管理器Compat.setAlarmClock这些应用的方法,因为警报的主要目的是通知用户预定的呼叫。 的setAlarmClock方法将警报数量限制为每15分钟最多1个,因此,如果您的应用需要安排运行时间以不通知用户,或者需要每15分钟运行一次以上的代码,则应使用其他方法 的报警管理器要么报警管理器Compat classes,要么use some different approach.

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import androidx.core.app.AlarmManagerCompat;

public class MyAlarms extends BroadcastReceiver
    private AlarmManager alarmManager;
    private Context appContext;
    private final static int REQUEST_CODE = 1;

    // The current application context must be passed into this constructor
    public MyAlarms(Context appContext) {
        this.appContext = appContext;

        // Get the AlarmManager
        alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
    }

    public void setAlarm(long timeInMilliseconds) {
        // Create an intent that references this class
        Intent intent = new Intent(context, getClass());

        // Create a pending intent (an intent to be used later)
        // If an identical pending intent already exists, the FLAG_UPDATE_CURRENT
        // flag ensures to not create duplicates
        PendingIntent pendingIntent = PendingIntent.getBroadcast(
            appContext, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        );

        // Set the alarm to call the onReceive method at the selected time
        AlarmManagerCompat.setAlarmClock(alarmManager, timeInMilliseconds, pendingIntent, pendingIntent);
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        // This method will get called when the alarm clock goes off
        // Put the code to execute here
    }
}

要设置警报,请创建一个实例我的警报并致电setAlarm方法,传入毫秒时间戳以获取所需的警报时间:

// From inside an Activity or a Service:
MyAlarms myAlarms = new MyAlarms(this);

// Set the alarm to go off after an hour
// An hour = 60 minutes * 60 seconds * 1000 milliseconds
long afterAnHour = System.currentTimeMillis() + 60 * 60 * 1000;
myAlarms.setAlarm(afterAnHour);

Detecting device reboots

使用的一个问题报警管理器是当用户重新启动设备时所有计划的警报都会丢失。 为了使警报即使在重启后也能正常工作,您的应用需要检测设备在重启,并在重启后重新安排警报。 这需要您将警报时间保存在某些持久性存储中,例如共享首选项,当设置了警报时,在检测到重新启动时从存储中读取信息,并再次计划相同的警报。

操作系统将广播消息发送到所有监听的应用程序BOOT_COMPLETED动作。 要通知您的应用,请先声明RECEIVE_BOOT_COMPLETED权限,并添加一个目的过滤器到您的接收器中AndroidManifest.xml文件:

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

  <application ...>
    ...

    <receiver android:name=".MyAlarms" android:enabled="true">
      <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
      </intent-filter>
    </receiver>

  </application>
</manifest>

然后在你的广播接收器实施,扩大onReceive检查收到的消息类型并根据需要重新安排警报的方法。 另外,在安排警报时,请将警报时间保存在SharedPreferences中。

public class MyAlarms extends BroadcastReceiver
    private AlarmManager alarmManager;
    private Context appContext;
    private final static int REQUEST_CODE = 1;
    private final static long TIME_NOT_SET = 0;

    public MyAlarms(Context appContext) {
        this.appContext = appContext;
        alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
    }

    public void setAlarm(long timeInMilliseconds) {
        Intent intent = new Intent(context, getClass());
        PendingIntent pendingIntent = PendingIntent.getBroadcast(
            appContext, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        );
        AlarmManagerCompat.setAlarmClock(alarmManager, timeInMilliseconds, pendingIntent, pendingIntent);

        // Open shared preferences and save the alarm time
        SharedPreferences prefs = appContext.getSharedPreferences("alarms", Context.MODE_PRIVATE);
        SharedPreferences.Editor prefsEditor = prefs.edit();
        prefsEditor.putLong("alarmtime", timeInMilliseconds);
        prefsEditor.apply();
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        // Check if this broadcast message is about a device reboot
        if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
            // Yes it is! Get the last saved alarm time from shared preferences
            SharedPreferences prefs = appContext.getSharedPreferences("alarms", Context.MODE_PRIVATE);
            long savedAlarmTime = prefs.getLong("alarmtime", TIME_NOT_SET);

            // Is there a saved alarm time?
            if (savedAlarmTime != TIME_NOT_SET) {
                // Reschedule the alarm!
                setAlarm(savedAlarmTime);
            }
        }
        else {
            // This is not a device reboot, so it must be the alarm
            // clock going off. Do what your app needs to do.
        }
    }
}

Showing notifications 🚩

The main purpose of these apps is to notify the user when it's time to call. First of all, you'll need to create at least one Notification Channel, so that your app works in Android Oreo (version 26) and later. By creating channels, users can allow or deny notifications depending on their content. Be sure to provide good names and descriptions for your channels.

NotificationCompat

通知是在Android的发展历程中发生了很大变化的概念之一,因此,根据用户设备运行的Android版本不同,存在很多不同的处理方法。 幸运的是,AndroidX包含NotificationCompat和NotificationManagerCompat消除了某些痛苦的课程。

public class MyNotifications {
    private final static String CHANNEL_ID = "MAIN";
    private final static int ID = 12345;
    private final static int IMPORTANCE = NotificationManager.IMPORTANCE_DEFAULT;

    // You should definitely get the NAME and DESCRIPTION from resources!
    private final static String NAME = "Call reminders";
    private final static String DESCRIPTION = "These notifications remind you to call your mom";

    public void createChannel(Context context) {
        // Only do this if running Android Oreo or later
        if (Build.VERSION.SDK_INT <>= Build.VERSION_CODES.O) return;

        // Get the NotificationManager
        NotificationManager notificationManager = context.getSystemService(NotificationManager.class);

        // Create and configure the channel
        NotificationChannel channel = new NotificationChannel(CHANNEL_ID, NAME, IMPORTANCE);
        channel.setDescription(DESCRIPTION);
        channel.setShowBadge(true);
        channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);

        // Create the channel
        notificationManager.createNotificationChannel(channel);
    }

    // When a channel has been created, call this method to show the
    // notification, and pass a PendingIntent that will get started
    // when the user clicks the notification; preferably you will
    // pass an Activity intent to start.
    public void showNotification(Context context, String title, String text, PendingIntent intentToStart) {
        NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
            .setSmallIcon(R.drawable.my_notification_icon)
            .setCategory(NotificationCompat.CATEGORY_REMINDER)
            .setContentTitle(title)
            .setContentText(text)
            .setContentIntent(intentToStart)
            .setOnlyAlertOnce(true)
            .setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL);

        NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
        notificationManager.notify(ID, builder.build());
    }
}

Summary 🔖

  • For compatibility and modern UI elements, ignore the older Support Library, and use AndroidX
  • Implement simple key-value persistant storage with SharedPreferences
  • Do more complex persistent data storage with Room
  • Use Room entities and other app state with LiveData
  • To allow alarms to survive device restarts, listen for the BOOT_COMPLETED message
  • Show notifications correctly using NotificationCompat

Cover photo by Daria Nepriakhina on Unsplash

from: https://dev.to//atornblad/don-t-forget-to-build-that-app-2md6

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值