從頭開始,手寫android應用框架(一)

kiba518發表於2023-03-30

前言

搭建android專案框架前,我們需要先定義要框架的結構,因為android框架本身的結構就很複雜,如果一開始沒定義好結構,那麼後續的使用就會事倍功半。

結構如下:

com.kiba.framework

——activity 儲存所有的活動

  ——base 儲存baseActivity

——fragment儲存所有的Fragment

  ——base 儲存baseFragment

——service儲存所有的service

——utils儲存所有的工具類

——dto儲存所有的傳入傳出實體

——model儲存所有的實體類

——model_db儲存所有的資料庫實體類(框架使用ormlit)

建立專案

我們先建立一個專案,File—New—New Project,選擇BasicActivity。

然後建立一個utils資料夾。

新增LogUtils,DateUtils,DecimalUtil檔案,就是簡單的日誌輸出,日期,字串工具。(寫法很多,可以上網任意搜尋)。

然後建立一個異常捕獲檔案——CrashExceptionHandler,用於輸入未捕獲異常日誌(寫法很多,可以上網任意搜尋)。

然後開啟app下的gradle,引入我們常用的包。

網路請求:okhttp。

json處理:gson和fastjson。

黃油刀註解:ButterKnife。

內建資料庫管理:ormlite。

許可權請求:rxpermissions。

圖片處理:glide。

程式碼如下:

    //okhttp
    implementation "com.squareup.okhttp3:okhttp:4.9.0"
    //gson
    implementation 'com.google.code.gson:gson:2.8.6'
    //fastjson
    implementation group: 'com.alibaba', name: 'fastjson', version: '1.2.83'
    //解決超過65546程式碼的問題
    implementation 'com.android.support:multidex:1.0.2'
    implementation "com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.4"
    //ButterKnife
    implementation 'com.jakewharton:butterknife:10.2.3'
    annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.3'
    // 資料庫ormlite
    implementation 'com.j256.ormlite:ormlite-android:5.0'
    implementation 'com.j256.ormlite:ormlite-core:5.0'
    //許可權請求rxpermissions
    implementation 'com.tbruyelle.rxpermissions:rxpermissions:0.9.4@aar'
    //圖片處理glide
    implementation 'com.github.bumptech.glide:glide:4.14.2'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.14.2'//執行時 編譯時 處理註解

然後在新增一些json和http的utils(寫法很多,可以上網任意搜尋)。

然後建立MyApplication的java檔案,程式碼如下:

public class MyApplication extends Application {
 
    public static Context context;//全域性上下文
    public static List<Activity> activityList = new ArrayList<Activity>();//用於存放所有啟動的Activity的集合
    public static ApplicationInfo applicationInfo;
​
    @Override
    public void onCreate() {
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Log.d("專案啟動", "專案啟動: " + DateUtils.getTime());
        super.onCreate();
​
        context = getApplicationContext();
​
        PackageManager packageManager = getApplicationContext().getPackageManager();
        try {
            packageManager = getApplicationContext().getPackageManager();
            applicationInfo = packageManager.getApplicationInfo(getPackageName(), 0);
        } catch (PackageManager.NameNotFoundException e) {
            applicationInfo = null;
            LogUtils.LogHelperError("獲取applicationInfo報錯", e);
        }
​
        CrashExceptionHandler.getInstance().init(this);
​
        //解決4.x執行崩潰的問題
        MultiDex.install(this);
​
    }
​
    private boolean isDebug() {
        return BuildConfig.DEBUG;
    }
    public static String GetProperties(String propertyName) {
        Properties props = new Properties();
        String serviceUrl = null;
        try {
            InputStream in =context.getAssets().open("appConfig.properties");
            props.load(in);
            String vaule = props.getProperty(propertyName);
            serviceUrl = new String(vaule.getBytes("ISO-8859-1"), "gbk");
        } catch (IOException e) {
            e.printStackTrace();
            AlertDialog.Builder dialog = new AlertDialog.Builder(context);
            dialog.setTitle("錯誤");
            dialog.setMessage("讀取配置檔案失敗");
            dialog.setCancelable(false);
            removeALLActivity();
        }
        return serviceUrl;
    }
    /**
     * 銷燬所有的Activity
     */
    public static void removeALLActivity() {
        //透過迴圈,把集合中的所有Activity銷燬
        for (Activity activity : activityList) {
            if (!activity.isFinishing()) {
                activity.finish();
            }
        }
        MyApplication.activityList.clear();
    }
​
}

然後註冊CrashException和MultiDex。

然後找到AndroidManifest.xml,註冊application,並開啟大堆記憶體,如下:

HTTP請求

http請求是我們最常用的工具,下面我們編寫一個簡單的請求工具。

先建立一個資料夾dto,然後在建立一個base,一個user資料夾。

編寫簡單的請求和返回實體如下:

然後編寫HttpUtils程式碼如下:

public class HttpUtils {
    private static final OkHttpClient client = new OkHttpClient.Builder()
            .sslSocketFactory(SSLSocketClient.getSSLSocketFactory(), SSLSocketClient.getX509TrustManager())
            .hostnameVerifier(SSLSocketClient.getHostnameVerifier())
            .connectTimeout(10, TimeUnit.SECONDS)//設定連線超時時間
            .readTimeout(20, TimeUnit.SECONDS)//設定讀取超時時間
            .build();
​
​
    private static void getRequest(String url, ICallback callback) throws IOException {
        new Thread() {
            @Override
            public void run() {
                Request request = new Request.Builder()
                        .url(url)
                        .build();
​
                try (Response response = client.newCall(request).execute()) {
                    String result = response.body().string();
                    callback.Call(result);
                } catch (IOException e) {
                    e.printStackTrace();
                    Log.d("http異常", e.getMessage());
                    callback.Call(e.getMessage());
​
                }
            }
        }.start();
​
    }
​
    private static final MediaType mediaType  = MediaType.get("application/json; charset=utf-8");
​
    private static void postRequest(String url, String param, ICallback callback) throws IOException {
        new Thread() {
            @Override
            public void run() {
​
                RequestBody body = RequestBody.create(mediaType, param);
                Request request = new Request.Builder()
                        .url(url)
                        .post(body)
                        .build();
​
                try (Response response = client.newCall(request).execute()) {
                    String result = response.body().string();
                    callback.Call(result);
                } catch (IOException e) {
                    e.printStackTrace();
                    BaseResult baseResult=new BaseResult();
                    baseResult.code=-1;
                    callback.Call(JsonUtils.Serialize(baseResult)); 
                }
            }
        }.start(); 
    }
​
    private interface ICallback {
        void Call(String con);
    }
    public interface HttpData<T>{
        public void getData(T result); 
    }
    public static <T> void post(String param, String urlAddress, HttpData<T> httpData) {
​
        try {
            HttpUtils.postRequest(urlAddress, param, con -> {
                runOnUiThread(() -> {
​
                    BaseResult baseResult = JsonUtils.Deserialize(BaseResult.class, con);
                    if (null != baseResult && baseResult.code == 1) {
                        Class thisClass = httpData.getClass();
​
                        Type[] superClassType = thisClass.getGenericInterfaces();
                        ParameterizedType pt = (ParameterizedType) superClassType[0];
​
                        Type[] genTypeArr = pt.getActualTypeArguments();
                        Type genType = genTypeArr[0];
                        Class c1= (Class) genTypeArr[0];
​
                        T result = (T)JsonUtils.Deserialize(c1, con);
                        httpData.getData(result);
​
                    } else {
                        if (null != baseResult) {
                            ToastUtils.showToast("資料獲取失敗:" + baseResult.msg);
                        } else {
                            ToastUtils.showToast("資料獲取失敗");
                        }
                    } 
                });
            });
        } catch (IOException e) {
            e.printStackTrace();
        } 
    }
    BaseResult baseResult = (BaseResult)JsonUtils.Deserialize(BaseResult.class, "con");
    public static <T> void get(String param, String urlAddress, HttpData<T> httpData) {
​
        try {
​
            HttpUtils.getRequest(urlAddress, con -> { 
                runOnUiThread(() -> { 
                    BaseResult baseResult = (BaseResult)JsonUtils.Deserialize(Object.class, con);
                    if (null != baseResult && baseResult.code == 1) {
                        Class thisClass = httpData.getClass();
​
                        Type[] superClassType = thisClass.getGenericInterfaces();
                        ParameterizedType pt = (ParameterizedType) superClassType[0];
​
                        Type[] genTypeArr = pt.getActualTypeArguments();
                        Type genType = genTypeArr[0];
                        Class c1= (Class) genTypeArr[0];
​
                        T result = (T)JsonUtils.Deserialize(c1, con);
                        httpData.getData(result);
​
                    } else {
                        if (null != baseResult) {
                            ToastUtils.showToast("資料獲取失敗:" + baseResult.msg);
                        } else {
                            ToastUtils.showToast("資料獲取失敗");
                        }
                    } 
                });
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

這裡透過泛型反射直接找到了要序列化的型別,減少了呼叫時的程式碼編寫,呼叫程式碼如下:

HttpUtils.get("url","引數", new HttpHelper.HttpData<LoginCommandResult>() {
     @Override
     public void getData(LoginCommandResult result) {
         int code = result.code;
     }
 });

簡單的輸入引數和url後,就可以在匿名類的重寫函式中獲得返回值。

編寫Activity與Fragment

應用的頁面切換是以Fragment的替換為主,以儘量少建立Activity為中心思想,框架實現返回按鈕切換fragment。

Activity於Fragment的編寫思路如下:

首先編寫Base檔案,Base檔案這裡採取二級模式,BaseActivity加KActivity、BaseFragment加KFragment。

KBase檔案實現生命週期,Base檔案實現通用函式。

KActivity程式碼簡介:

 /**
     * @return 獲取佈局的id
     */
    protected int getLayoutId() {
        return -1;
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        int layoutId = getLayoutId();
        if (layoutId != -1) {
            this.rootView = View.inflate(this, layoutId, null);
            setContentView(this.rootView);
        } else {
            throw new MissingResourceException("未使用getLayoutId()函式初始化view",this.getClass().getName(),"未初始化view");
        }
        if (savedInstanceState != null) { 
           loadActivitySavedData(savedInstanceState);
        } 
    }

Base檔案裡將設定佈局檔案給提取出來了,並設定從Bundle裡恢復資料的操作。

繼承Base檔案的Activity實現如下:

public class MainActivity extends BaseActivity {
​
    @Override
    protected int getLayoutId() {
        return R.layout.activity_main;
    }
​
    @Override
    protected void onCreate(Bundle savedInstanceState) {
​
        super.onCreate(savedInstanceState);
​
    }
    /**
     * 選單、返回鍵響應
     */
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            //moveTaskToBack(true);
        }
        return true;
    }
}

繼承Base檔案的Fragment實現如下:

public class MainFragment extends BaseFragment { 
    @Override
    protected int getLayoutId() {
        return R.layout.fragment_main;
    }
​
    @Override
    protected void onCreate() {
        
    } 
}

可以看到,在類裡,使用getLayoutId來指定佈局XML檔案,這樣即可清晰的知道佈局檔名,又便於閱讀。

PS:Android是支援多個Activity或Fragment使用同一個XML的,但本框架中,拒絕這個特性,要求佈局檔案與類檔案是一對一的關係。

gradle配置

app.gradle

開啟app的gradle,首先在defaultConfig下增加指定cpu。

 ndk {
            abiFilters 'armeabi', 'armeabi-v7a', 'x86'
        }

然後在android下面關閉lint檢測。

//不在googlePlay上線,關閉lint檢測
    lintOptions {
        checkReleaseBuilds false
        abortOnError false
    }

project.gradle

我用的新版本AS建的專案,所以預設的程式碼是這樣的。

plugins {
    id 'com.android.application' version '7.3.1' apply false
    id 'com.android.library' version '7.3.1' apply false
}

這裡我們直接將生成的配置刪除,貼上上我們比較熟悉的gradle配置模式,程式碼如下:

buildscript {
    repositories {
        maven { url 'https://jitpack.io' }
        maven { url "https://oss.jfrog.org/libs-snapshot" }//rxjava
        google()
        mavenCentral()
        jcenter() // Warning: this repository is going to shut down soon
​
    }
    dependencies {
​
        classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3'
        classpath 'com.android.tools.build:gradle:7.1.2'
​
    }
}
​
allprojects {
    repositories {
        maven { url 'https://jitpack.io' }
        maven { url "https://oss.jfrog.org/libs-snapshot" }//rxjava
        google()
        mavenCentral()
        jcenter() // Warning: this repository is going to shut down soon
​
    }
}
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}
task clean(type: Delete) {
    delete rootProject.buildDir
}

要注意的是,新版的settings.gradle也變化了,如果只修改build.gradle編譯會拋異常。

生成的setting.gradle程式碼如下:

pluginManagement {
    repositories {
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}
rootProject.name = "framework"
include ':app'

修改程式碼如下:

rootProject.name = "framework"
include ':app'

首頁佈局

結構搭建好後,我們使用LinkageRecyclerView元件,實現一個簡單的雙列表佈局,介面如下:

結語

最後我們看一下專案結構,如下圖:

 如上圖,一個簡單的,有序的,支援activity恢復資料,支援fragment返回的框架就搭建完成了。

----------------------------------------------------------------------------------------------------

到此,手寫Android框架一就已經介紹完了。

程式碼已經傳到Github上了,歡迎大家下載。

下篇文章介紹AspectJX實現AOP的幾個實用註解。

Github地址:https://github.com/kiba518/AndroidFramework2.0/

----------------------------------------------------------------------------------------------------

注:此文章為原創,任何形式的轉載都請聯絡作者獲得授權並註明出處!
若您覺得這篇文章還不錯,請點選下方的推薦】,非常感謝!

https://www.cnblogs.com/kiba/p/17262561.html