Android 动态化多语言框架,支持语言包的动态下发、升级、删除,一处安装,到处使用

Overview

MLang 动态化多语言框架

MLang 是 MultiLanguage 的简写,是一款动态化的多语言框架。

设计优雅

  • 语言包存储格式为 xml 格式,和 res 下的 strings.xml 一致
  • 零依赖,完全使用系统 api 和系统的 xml 解析器
  • 不持有 context,无内存泄漏
  • 静态方法 + 单例模式,一处安装,到处使用

动态化语言包

  • 动态下发语言包
  • 语言包的增加、升级、删除
  • 语言包内部任意字符串的增加、升级、删除
  • 自定义语言包的存储路径

完全兼容

  • 跟随系统语言
  • 时间格式跟随系统的 24 小时制
  • 处理各种语言的时区、时间格式化问题
  • 处理各种语言的复数格式化问题
  • 处理各种语言的阅读顺序问题(从左到右、从右到左)

1. 使用

使用字符串

// 本地和云端都存在的字符串
MyLang.getString("local_string", R.string.local_string)

// 云端存在 remote_string_only
// 但本地没有 R.string.remote_string_only,用 R.string.fallback_string 代替
MyLang.getString("remote_string_only", R.string.fallback_string)

使用语言包

//应用一种语言(这里自动处理了语言包的升级、语言包内部字符串的升级)
MyLang.getInstance().applyLanguage(Context, LocaleInfo, force=true, init=false);

//删除一种语言
MyLang.getInstance().deleteLanguage(Context, LocaleInfo);

LocaleInfo 可以在以下地方找到

//1. 所有云端的语言包
MyLang.getInstance().remoteLanguages

//2. 所有下载到本地、可用的语言包
MyLang.getInstance().languages

//3. 所有非官方的语言包
MyLang.getInstance().unofficialLanguages

//4. 除内置支持的语言外,另外安装的云端的语言包
MyLang.getInstance().otherLanguages

2. 安装

2.1. 引入

//build.gradle
allprojects {
    repositories {
        google()
        jcenter()
        maven { url "https://github.com/LinXueyuanStdio/MLang/raw/main/dist/" }
    }
}

//app/build.gradle
implementation 'com.timecat.component:MLang:2.0.2'

2.2. 在 Application 中初始化,并监听系统语言的更改(如果跟随系统语言的话):

public class MyApplication extends Application {
    @SuppressLint("StaticFieldLeak")
    public static volatile Context applicationContext;
    public static volatile Handler applicationHandler;

    @Override
    public void onCreate() {
        super.onCreate();
        applicationContext = this;
        applicationHandler = new Handler(applicationContext.getMainLooper());
        MyLang.init(applicationContext);
    }

    @Override
    public void onConfigurationChanged(@NonNull Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        MyLang.onConfigurationChanged(newConfig);
    }
}

其中建议自己新建一个静态类 MyLang 来代理 MLang。 这样有两个好处:

  1. 隔绝 MLang 的 api 变化,提高兼容性和稳定性。
  2. 使用更简洁。MLang 不持有 context,但每次获取字符串为空时,需要 context 来兜底,获取本地的字符串。在自己的 MyLang 默认提供 application Context,可以不用到处提供 context,更简洁。
public class MyLang {
    private static File filesDir;
    private static LangAction action;
    public static void init(@NonNull Context applicationContext) {
        filesDir = applicationContext.getCacheDir();
        action = new MyLangAction();
        getInstance();
    }
    public static MLang getInstance() {
        return MLang.getInstance(MyApplication.applicationContext, filesDir, action);
    }
    public static void onConfigurationChanged(@NonNull Configuration newConfig) {
        getInstance().onDeviceConfigurationChange(getContext(), newConfig);
    }
}

3. 设计

3.1. 单例模式接收 3 个参数,context,fileDir,action

  1. context:MLang 内部不持有该 context。该 context 用于注册时区广播(根据时区来格式化字符串中的时间)、 判断系统当前时间是否 24 小时制等等。
  2. filesDir:持久化语言包文件的存储地址。语言包文件是 xml 格式,和 res 下的 strings.xml 一样。
  3. action:action 包含了应用语言包、切换语言等等需要的所有回调,即 LangAction 接口。
MLang.getInstance(context, filesDir, action);

3.2. LangAction 接口定义了 2 个东西

  1. 当前语言的设置存储。 MLang 根据语言 id (string) 来识别当前语言。语言 id 需要持久化。 所以设计了下面两个方法,可以自行决定持久化的方式(SharedPreferences、MMKV、SQLite等等)。
    void saveLanguageKeyInLocal(String language);
    @Nullable String loadLanguageKeyInLocal();
  2. 必要的网络接口。
    void langpack_getDifference(String lang_pack, String lang_code, int from_version, @NonNull final LangAction.GetDifferenceCallback callback)
    void langpack_getLanguages(@NonNull final LangAction.GetLanguagesCallback callback)
    void langpack_getLangPack(String lang_code, @NonNull final LangAction.GetLangPackCallback callback)

LangAction 的注释如下:

languageList); } interface GetDifferenceCallback { /** * 必须在UI线程或者主线程调用 * 如果服务端没有实现增量分发的功能,可以用完整的语言包代替 * @param languageList 增量的语言包 */ void onLoad(LangPackDifference languageList); } interface GetLangPackCallback { /** * 必须在UI线程或者主线程调用 * @param languageList 完整的语言包 */ void onLoad(LangPackDifference languageList); } } ">
public interface LangAction {
    /**
     * SharedPreferences preferences = Utilities.getGlobalMainSettings();
     * SharedPreferences.Editor editor = preferences.edit();
     * editor.putString("language", language);
     * editor.commit();
     * @param language localeInfo.getKey() 语言 id
     */
    void saveLanguageKeyInLocal(String language);

    /**
     * SharedPreferences preferences = Utilities.getGlobalMainSettings();
     * String lang = preferences.getString("language", null);
     * @return @Nullable lang 语言 id
     */
    String loadLanguageKeyInLocal();

    /**
     * 在其他线程网络请求,在主线程或UI线程调用callback
     * 这里设计成这样,是因为这个方法里支持异步执行
     * 您需要在合适的时机手动调用 callback,且只能调用一次
     * @param lang_pack 语言包名字
     * @param lang_code 语言包版本名称
     * @param from_version 语言包版本号
     * @param callback @NonNull 在主线程或UI线程调用
     */
    void langpack_getDifference(String lang_pack, String lang_code, int from_version, GetDifferenceCallback callback);

    /**
     * 在其他线程网络请求,在主线程或UI线程调用callback
     * 这里设计成这样,是因为这个方法里支持异步执行
     * 您需要在合适的时机手动调用 callback,且只能调用一次
     * @param callback @NonNull 在主线程或UI线程调用
     */
    void langpack_getLanguages(GetLanguagesCallback callback);

    /**
     * 在其他线程网络请求,在主线程或UI线程调用callback
     * 这里设计成这样,是因为这个方法里支持异步执行
     * 您需要在合适的时机手动调用 callback,且只能调用一次
     * @param lang_code 语言包版本名称
     * @param callback @NonNull 在主线程或UI线程调用
     */
    void langpack_getLangPack(String lang_code, GetLangPackCallback callback);

    interface GetLanguagesCallback {
        /**
         * 必须在UI线程或者主线程调用
         * 所有可用的语言包
         * @param languageList 语言包列表
         */
        void onLoad(List<LangPackLanguage> languageList);
    }

    interface GetDifferenceCallback {
        /**
         * 必须在UI线程或者主线程调用
         * 如果服务端没有实现增量分发的功能,可以用完整的语言包代替
         * @param languageList 增量的语言包
         */
        void onLoad(LangPackDifference languageList);
    }

    interface GetLangPackCallback {
        /**
         * 必须在UI线程或者主线程调用
         * @param languageList 完整的语言包
         */
        void onLoad(LangPackDifference languageList);
    }

}

实现LangAction的一个示例如下:

languageList) { callback.onLoad(languageList); } }); } @Override public void langpack_getLangPack(String lang_code, @NonNull final LangAction.GetLangPackCallback callback) { Server.request_langpack_getLangPack(lang_code, new Server.GetLangPackCallback() { @Override public void onNext(final LangPackDifference difference) { callback.onLoad(difference); } }); } } ">
public class MyLangAction implements LangAction {
   @Override
   public static void saveLanguageKeyInLocal(String language) {
       SharedPreferences preferences = getContext().getSharedPreferences("language_locale", Context.MODE_PRIVATE);
       SharedPreferences.Editor editor = preferences.edit();
       editor.putString("language", language);
       editor.apply();
   }

   @Override
   @Nullable
   public static String loadLanguageKeyInLocal() {
       SharedPreferences preferences = getContext().getSharedPreferences("language_locale", Context.MODE_PRIVATE);
       return preferences.getString("language", null);
   }
   @Override
   public void langpack_getDifference(String lang_pack, String lang_code, int from_version, @NonNull final LangAction.GetDifferenceCallback callback) {
       Server.request_langpack_getDifference(lang_pack, lang_code, from_version, new Server.GetDifferenceCallback() {
           @Override
           public void onNext(final LangPackDifference difference) {
               callback.onLoad(difference);
           }
       });
   }

   @Override
   public void langpack_getLanguages(@NonNull final LangAction.GetLanguagesCallback callback) {
       Server.request_langpack_getLanguages(new Server.GetLanguagesCallback() {
           @Override
           public void onNext(final List<LangPackLanguage> languageList) {
               callback.onLoad(languageList);
           }
       });
   }

   @Override
   public void langpack_getLangPack(String lang_code, @NonNull final LangAction.GetLangPackCallback callback) {
       Server.request_langpack_getLangPack(lang_code, new Server.GetLangPackCallback() {
           @Override
           public void onNext(final LangPackDifference difference) {
               callback.onLoad(difference);
           }
       });
   }
}

3.3. 服务器语言包的结构

模拟的服务器数据

语言包实体

  • LangPackLanguage(name, version, ...)

语言包的数据

  • LangPackDifference(name, version, List , ...)
  • LangPackString(key: String, value: String)
chineseStrings() { ArrayList list = new ArrayList<>(); list.add(new LangPackString("LanguageName", "中文简体")); list.add(new LangPackString("LanguageNameInEnglish", "Chinese")); list.add(new LangPackString("local_string", "中文的云端字符串")); list.add(new LangPackString("remote_string_only", "本地缺失,云端存在的字符串")); return list; } } ">
public class Server {
    public static LangPackLanguage chineseLanguage() {
        LangPackLanguage langPackLanguage = new LangPackLanguage();
        langPackLanguage.name = "chinese";
        langPackLanguage.native_name = "简体中文";
        langPackLanguage.lang_code = "zh";
        langPackLanguage.base_lang_code = "zh";
        return langPackLanguage;
    }
    public static LangPackDifference chinesePackDifference() {
        LangPackDifference difference = new LangPackDifference();
        difference.lang_code = "zh";
        difference.from_version = 0;
        difference.version = 1;
        difference.strings = chineseStrings();
        return difference;
    }
    public static ArrayList<LangPackString> chineseStrings() {
        ArrayList<LangPackString> list = new ArrayList<>();
        list.add(new LangPackString("LanguageName", "中文简体"));
        list.add(new LangPackString("LanguageNameInEnglish", "Chinese"));
        list.add(new LangPackString("local_string", "中文的云端字符串"));
        list.add(new LangPackString("remote_string_only", "本地缺失,云端存在的字符串"));
        return list;
    }
}

4. 进阶配置

MLang.isRTL = false; //是否从右到左阅读(默认 false)
MLang.is24HourFormat = false; //是否 24 小时制(默认 false)
MLang.USE_CLOUD_STRINGS = true; //是否使用云端字符串(默认 true)
You might also like...
Rosie is an Android framework to create applications following the principles of Clean Architecture.
Rosie is an Android framework to create applications following the principles of Clean Architecture.

Rosie The only way to make the deadline—the only way to go fast—is to keep the code as clean as possible at all times. — Robert C. Martin in Clean Cod

Create kotlin android project with one line of command.

README This is an android application template project built with kotlin language and some useful libraries. It provides a creator script to quickly c

Moxy is MVP library for Android
Moxy is MVP library for Android

Moxy This Moxy repository is deprecated and no longer supported. Please migrate to the actual version of the Moxy framework at Moxy communuty repo. De

A data-binding Presentation Model(MVVM) framework for the Android platform.

PLEASE NOTE, THIS PROJECT IS NO LONGER BEING MAINTAINED. As personal time contraints, I am currently unable to keep up. Please use official android da

Minimal UI library for Android inspired by React
Minimal UI library for Android inspired by React

Anvil - reactive views for Android Anvil is a small Java library for creating reactive user interfaces. Originally inspired by React, it suits well as

A full-featured framework that allows building android applications following the principles of Clean Architecture.

EasyMVP A powerful, and very simple MVP library with annotation processing and bytecode weaving. EasyMVP eliminates the boilerplate code for dealing w

a MVP library for Android favoring a stateful Presenter

DEPRECATED - no longer actively maintained ThirtyInch - a MVP library for Android This library adds Presenters to Activities and Fragments. It favors

Android Next 公共组件库

Android-Next 公共组件库 这个库是我在日常开发过程中积累下来的一些可复用组件,大部分都在我的工作项目和个人项目中有使用。 最新版本: Gradle集成 // core 核心库, 格式:jar和aar compile 'com.mcxiaoke.next:core:1.7.

Pet project using Clean Architecture + MVVM + Reactive Extensions + Android Architecture Components. The data are fetched from LondonTheatreDirect API. 🎭
Pet project using Clean Architecture + MVVM + Reactive Extensions + Android Architecture Components. The data are fetched from LondonTheatreDirect API. 🎭

Theatre Pet project using Clean Architecture + MVVM + Reactive Extensions + Android Architecture Components. The data is fetched from LondonTheatreDir

Comments
  • 【思考】如何在内存占用和运行速度中权衡

    【思考】如何在内存占用和运行速度中权衡

    本框架是一个用于字符串动态管理的框架,最常用的场景是按关键字获取字符串,主要的优化目标也是这个场景。

    按关键字获取字符串,现有两种方法,稍微分析一下。

    1. 一次性读取当前语言包,把语言包缓存到 static map 中
      1. 运行速度最快,因为字符串在内存里
      2. 内存占用最大,因为整个语言包都加载进内存,很多没有用到,造成内存浪费
    2. 将语言包保持为文件,每次用到的时候再读取到内存,然后使用
      1. 内存占用小,因为只有用到的字符串才加载进内存
      2. 首次运行速度慢,因为第一次加载要频繁从文件读取字符串

    我希望综合以上两种方式的优点

    1. 首次运行速度要快,内存可以占用高一点
    2. 后面每次运行速度要尽可能快,内存占用要低

    现暂时考虑以下方法

    1. 第一次打开app,一次性读取当前语言包,缓存到 static map中。
    2. 每次使用字符串,给字符串统计使用次数。使用完后,按次数排序,固化到文件,杀死app。
    3. 第二次打开app,读取使用次数最高的 k 个字符串常驻内存,后面有新的字符串再临时从文件读取,返回 2。

    因为第一次读取全部语言包到内存,使用时速度最快,内存占用较高,可接受 第二次读取使用次数最高的 k 个字符串到内存。如果用户第二次使用的字符串和第一次相同,那这个就是最优。

    opened by LinXueyuanStdio 1
Owner
兮尘
写 OOP 久了,竟感觉每个模块,每个对象都有生命,而我的使命,就是守护这些可爱的生命。
兮尘
Android common lib, include ImageCache, HttpCache, DropDownListView, DownloadManager, Utils and so on

android-common-lib 关于我,欢迎关注 微博:Trinea 主页:trinea.cn 邮箱:trinea.cn#gmail.com 微信:codek2 主要包括:缓存(图片缓存、预取缓存、网络缓存)、公共View(下拉及底部加载更多ListView、底部加载更多ScrollView、

Trinea 5k Dec 30, 2022
A Model-View-Presenter / Model-View-Intent library for modern Android apps

Mosby A Model-View-Presenter and Model-View-Intent library for Android apps. Dependency dependencies { compile 'com.hannesdorfmann.mosby3:mvi:3.1.1

Hannes Dorfmann 5.5k Dec 25, 2022
dexposed enable 'god' mode for single android application.

What is it? Dexposed is a powerful yet non-invasive runtime AOP (Aspect-oriented Programming) framework for Android app development, based on the work

Alibaba 4.5k Dec 28, 2022
A small, yet full-featured framework that allows building View-based Android applications

Conductor A small, yet full-featured framework that allows building View-based Android applications. Conductor provides a light-weight wrapper around

BlueLine Labs 3.9k Jan 6, 2023
A Job Queue specifically written for Android to easily schedule jobs (tasks) that run in the background, improving UX and application stability.

This Project is Deprecated! Thanks to everybody who've used Android Priority JobQueue. It was designed in a world where there was no JobScheduler, RxJ

Yigit Boyar 3.4k Dec 31, 2022
A plugin system that runs like a browser, but instead of load web pages, it load apk plugins which runs natively on Android system.

Android Dynamic Loader Android Dynamic Loader is a plugin system. The host application is like a browser, but instead of load web pages, it load plugi

Tu Yimin 1.4k Dec 28, 2022
Nucleus is an Android library, which utilizes the Model-View-Presenter pattern to properly connect background tasks with visual parts of an application.

Nucleus Deprecation notice Nucleus is not under develpment anymore. It turns out that Redux architecture scales way better than MVP/MVI/MVVM/MVxxx and

Konstantin Mikheev 2k Nov 18, 2022
LiteOrm is a fast, small, powerful ORM framework for Android. LiteOrm makes you do CRUD operarions on SQLite database with a sigle line of code efficiently.

#LiteOrm:Android高性能数据库框架 A fast, small, powerful ORM framework for Android. LiteOrm makes you do CRUD operarions on SQLite database with a sigle line

马天宇 1.5k Nov 19, 2022
🚀Plugin for Android Studio And IntelliJ Idea to generate Kotlin data class code from JSON text ( Json to Kotlin )

JsonToKotlinClass Hi, Welcome! This is a plugin to generate Kotlin data class from JSON string, in another word, a plugin that converts JSON string to

Seal 2.8k Jan 3, 2023
Kick-starts Android application development.

Synopsis If you've made it here, chances are you are not quite as satisfied with the Android application framework as you could be. Same for us, that'

Matthias Käppler 1.3k Dec 4, 2022